From b36339ff9f20309c6c3331b75cdd24e21a0b2895 Mon Sep 17 00:00:00 2001 From: Bogdan Cilibiu Date: Wed, 4 Jul 2018 09:02:30 +0300 Subject: [PATCH 001/146] ifExperimental else --- src/app/directives/experimental.directive.ts | 44 +++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/app/directives/experimental.directive.ts b/src/app/directives/experimental.directive.ts index 3546f8c50d..09c30bed9d 100644 --- a/src/app/directives/experimental.directive.ts +++ b/src/app/directives/experimental.directive.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -import { Directive, TemplateRef, ViewContainerRef, Input } from '@angular/core'; +import { Directive, TemplateRef, ViewContainerRef, Input, EmbeddedViewRef } from '@angular/core'; import { AppConfigService, StorageService } from '@alfresco/adf-core'; import { environment } from '../../environments/environment'; @@ -32,6 +32,10 @@ import { environment } from '../../environments/environment'; selector: '[ifExperimental]' }) export class ExperimentalDirective { + private elseTemplateRef: TemplateRef; + private elseViewRef: EmbeddedViewRef; + private shouldRender: boolean; + constructor( private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef, @@ -43,19 +47,49 @@ export class ExperimentalDirective { const key = `experimental.${featureKey}`; const override = this.storage.getItem(key); + if (override === 'true') { - this.viewContainerRef.createEmbeddedView(this.templateRef); - return; + this.shouldRender = true; } if (!environment.production) { const value = this.config.get(key); if (value === true || value === 'true') { + this.shouldRender = true; + } + } + + if (override !== 'true' && environment.production) { + this.shouldRender = false; + } + + this.updateView(); + } + + @Input() set ifExperimentalElse(templateRef: TemplateRef) { + this.elseTemplateRef = templateRef; + this.elseViewRef = null; + this.updateView(); + } + + private updateView() { + if (this.shouldRender) { + this.viewContainerRef.clear(); + this.elseViewRef = null; + + if (this.templateRef) { this.viewContainerRef.createEmbeddedView(this.templateRef); + } + } else { + if (this.elseViewRef) { return; } - } - this.viewContainerRef.clear(); + this.viewContainerRef.clear(); + + if (this.elseTemplateRef) { + this.elseViewRef = this.viewContainerRef.createEmbeddedView(this.elseTemplateRef); + } + } } } From 9e5f4b58abbca0edbfe7fbd6494a6ce35497d56a Mon Sep 17 00:00:00 2001 From: Bogdan Cilibiu Date: Wed, 4 Jul 2018 09:03:34 +0300 Subject: [PATCH 002/146] config based comments view --- src/app.config.json | 3 +- .../info-drawer/info-drawer.component.html | 38 +++++++++++-------- src/assets/i18n/en.json | 3 +- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/app.config.json b/src/app.config.json index 0673d08960..7d6ef8fba1 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -9,7 +9,8 @@ "© 2017 - 2018 Alfresco Software, Inc. All rights reserved." }, "experimental": { - "libraries": false + "libraries": false, + "comments": false }, "headerColor": "#2196F3", "languagePicker": false, diff --git a/src/app/components/info-drawer/info-drawer.component.html b/src/app/components/info-drawer/info-drawer.component.html index 985ebec34e..828271da47 100644 --- a/src/app/components/info-drawer/info-drawer.component.html +++ b/src/app/components/info-drawer/info-drawer.component.html @@ -12,21 +12,29 @@ - - - - - + + + + + - -
- face - {{ 'VERSION.SELECTION.EMPTY' | translate }} -
-
-
+ + + + + + + + +
+ face + {{ 'VERSION.SELECTION.EMPTY' | translate }} +
+
+
+
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 189470c163..a9dfa968d9 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -232,7 +232,8 @@ "TITLE": "Details", "TABS": { "PROPERTIES": "Properties", - "VERSIONS": "Versions" + "VERSIONS": "Versions", + "COMMENTS": "Comments" } } }, From 1c5a70fa90f5cc50268453b18fc3c549bc6a7b02 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 4 Jul 2018 20:57:33 +0100 Subject: [PATCH 003/146] toggle comments from settings page --- .../settings/settings.component.html | 19 ++++++++++++++----- .../components/settings/settings.component.ts | 8 ++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index c17358f9ef..1e2a554ed0 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -59,10 +59,19 @@ {{ 'APP.SETTINGS.EXPERIMENTAL-FEATURES' | translate }} - - Library Management - +
+ + Library Management + +
+
+ + Comments + +
diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index e2ff08280a..ae4de7c52b 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -49,6 +49,7 @@ export class SettingsComponent implements OnInit { headerColor$: Observable; languagePicker$: Observable; libraries: boolean; + comments: boolean; constructor( private store: Store, @@ -74,6 +75,9 @@ export class SettingsComponent implements OnInit { const libraries = this.appConfig.get('experimental.libraries'); this.libraries = (libraries === true || libraries === 'true'); + + const comments = this.appConfig.get('experimental.comments'); + this.comments = (comments === true || comments === 'true'); } apply(model: any, isValid: boolean) { @@ -97,4 +101,8 @@ export class SettingsComponent implements OnInit { onChangeLibrariesFeature(event: MatCheckboxChange) { this.storage.setItem('experimental.libraries', event.checked.toString()); } + + onChangeCommentsFeature(event: MatCheckboxChange) { + this.storage.setItem('experimental.comments', event.checked.toString()); + } } From 656b92c610f4aa6bff4e65e09d42a40aa0ed32a2 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 4 Jul 2018 21:02:24 +0100 Subject: [PATCH 004/146] fix comment saving --- src/app/components/info-drawer/info-drawer.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/info-drawer/info-drawer.component.html b/src/app/components/info-drawer/info-drawer.component.html index 828271da47..a051dac503 100644 --- a/src/app/components/info-drawer/info-drawer.component.html +++ b/src/app/components/info-drawer/info-drawer.component.html @@ -14,7 +14,7 @@ - + From e5bc3bb755403f9b289fdd1cf3608ca3a3bdc580 Mon Sep 17 00:00:00 2001 From: Suzana Dirla Date: Thu, 5 Jul 2018 14:29:30 +0300 Subject: [PATCH 005/146] [ACA-1490] Enable CardView (#491) * integrate carview * experimental cardview * fix tests * fix spellcheck error --- cspell.json | 3 ++- src/app.config.json | 3 ++- src/app/components/favorites/favorites.component.html | 6 ++++++ src/app/components/favorites/favorites.component.spec.ts | 4 +++- src/app/components/files/files.component.html | 6 ++++++ src/app/components/files/files.component.spec.ts | 4 +++- src/app/components/libraries/libraries.component.html | 7 +++++++ src/app/components/page.component.ts | 8 +++++++- .../components/recent-files/recent-files.component.html | 6 ++++++ .../recent-files/recent-files.component.spec.ts | 4 +++- src/app/components/settings/settings.component.html | 7 +++++++ src/app/components/settings/settings.component.ts | 8 ++++++++ .../components/shared-files/shared-files.component.html | 6 ++++++ .../shared-files/shared-files.component.spec.ts | 4 +++- src/app/components/trashcan/trashcan.component.html | 6 ++++++ src/app/components/trashcan/trashcan.component.spec.ts | 4 +++- src/assets/i18n/en.json | 4 ++++ 17 files changed, 82 insertions(+), 8 deletions(-) diff --git a/cspell.json b/cspell.json index 6401bf8bda..fec81a42be 100644 --- a/cspell.json +++ b/cspell.json @@ -36,7 +36,8 @@ "tooltip", "tooltips", "unindent", - "exif" + "exif", + "cardview" ], "dictionaries": [ "html" diff --git a/src/app.config.json b/src/app.config.json index 7d6ef8fba1..c5b410bf84 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -10,7 +10,8 @@ }, "experimental": { "libraries": false, - "comments": false + "comments": false, + "cardview": false }, "headerColor": "#2196F3", "languagePicker": false, diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 330b340d93..113755bba6 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -3,6 +3,12 @@ + + + + + + + + + + + + + + + + + - + + + + + + + + - + + + + + + + + + + + + + + + diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index da4df81621..b334a2ca03 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -53,45 +53,17 @@ - + {{ 'APP.SETTINGS.EXPERIMENTAL-FEATURES' | translate }} -
- - Library Management - -
-
- - Comments - -
-
- - Cardview - -
-
- - Share - -
-
+
- Extensions + [(ngModel)]="flag.value" + (change)="onToggleExperimentalFeature(flag.key, $event)"> + {{ flag.key }}
diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 4ed9f18cda..497fe1b2b5 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -28,8 +28,8 @@ import { AppConfigService, StorageService, SettingsService } from '@alfresco/adf import { Validators, FormGroup, FormBuilder } from '@angular/forms'; import { Observable } from 'rxjs/Rx'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states'; -import { appLanguagePicker, selectHeaderColor, selectAppName } from '../../store/selectors/app.selectors'; +import { AppStore, ProfileState } from '../../store/states'; +import { appLanguagePicker, selectHeaderColor, selectAppName, selectUser } from '../../store/selectors/app.selectors'; import { MatCheckboxChange } from '@angular/material'; import { SetLanguagePickerAction } from '../../store/actions'; @@ -45,14 +45,11 @@ export class SettingsComponent implements OnInit { form: FormGroup; + profile$: Observable; appName$: Observable; headerColor$: Observable; languagePicker$: Observable; - libraries: boolean; - comments: boolean; - cardview: boolean; - share: boolean; - extensions: boolean; + experimental: Array<{ key: string, value: boolean }> = []; constructor( private store: Store, @@ -60,6 +57,7 @@ export class SettingsComponent implements OnInit { private settingsService: SettingsService, private storage: StorageService, private fb: FormBuilder) { + this.profile$ = store.select(selectUser); this.appName$ = store.select(selectAppName); this.languagePicker$ = store.select(appLanguagePicker); this.headerColor$ = store.select(selectHeaderColor); @@ -76,20 +74,14 @@ export class SettingsComponent implements OnInit { this.reset(); - const libraries = this.appConfig.get('experimental.libraries'); - this.libraries = (libraries === true || libraries === 'true'); - - const comments = this.appConfig.get('experimental.comments'); - this.comments = (comments === true || comments === 'true'); - - const cardview = this.appConfig.get('experimental.cardview'); - this.cardview = (cardview === true || cardview === 'true'); - - const share = this.appConfig.get('experimental.share'); - this.share = (share === true || share === 'true'); - - const extensions = this.appConfig.get('experimental.extensions'); - this.extensions = (extensions === true || extensions === 'true'); + const settings = this.appConfig.get('experimental'); + this.experimental = Object.keys(settings).map(key => { + const value = this.appConfig.get(`experimental.${key}`); + return { + key, + value: (value === true || value === 'true') + }; + }); } apply(model: any, isValid: boolean) { @@ -110,23 +102,7 @@ export class SettingsComponent implements OnInit { this.store.dispatch(new SetLanguagePickerAction(event.checked)); } - onChangeLibrariesFeature(event: MatCheckboxChange) { - this.storage.setItem('experimental.libraries', event.checked.toString()); - } - - onChangeCommentsFeature(event: MatCheckboxChange) { - this.storage.setItem('experimental.comments', event.checked.toString()); - } - - onChangeCardviewFeature(event: MatCheckboxChange) { - this.storage.setItem('experimental.cardview', event.checked.toString()); - } - - onChangeShareFeature(event: MatCheckboxChange) { - this.storage.setItem('experimental.share', event.checked.toString()); - } - - onChangeExtensionsFeature(event: MatCheckboxChange) { - this.storage.setItem('experimental.extensions', event.checked.toString()); + onToggleExperimentalFeature(key: string, event: MatCheckboxChange) { + this.storage.setItem(`experimental.${key}`, event.checked.toString()); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a040832520..10e678b8e1 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -4,6 +4,7 @@ "SIGN_IN": "Sign in", "SIGN_OUT": "Sign out", "SETTINGS": { + "TITLE": "Settings", "APPLICATION-SETTINGS": "Application Settings", "REPOSITORY-SETTINGS": "Repository Settings", "EXPERIMENTAL-FEATURES": "Experimental Features", From bc22053e2ea1351019ab2f7ccdeddaf016aeca91 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Sat, 7 Jul 2018 12:47:47 +0100 Subject: [PATCH 011/146] [ACA-1492] case insensitive mode for upload validation --- src/app.config.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app.config.json b/src/app.config.json index aed6b248cd..a7b4d95cd9 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -22,7 +22,10 @@ "supportedPageSizes": [25, 50, 100] }, "files": { - "excluded": [".DS_Store", "desktop.ini", "Thumbs.db", ".git"] + "excluded": [".DS_Store", "desktop.ini", "Thumbs.db", ".git"], + "match-options": { + "nocase": true + } }, "adf-version-manager": { "allowComments": true, From fe683015c52ac24b2897a67be00da0390c267942 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Sun, 8 Jul 2018 07:56:50 +0100 Subject: [PATCH 012/146] extensions: wave 2 (#497) * introduce "create folder" action * track opened folder via store * "create folder" action, support mulitple targets * fix card view colors and toolbar layouts * basic support for permissions * simplify create menu and add permissions * add toolbar separators for extension entries * "edit folder" extension command * minor code improvements --- src/app.config.json | 60 +++--- src/app/app.module.ts | 4 - .../services/browsing-files.service.spec.ts | 44 ----- .../common/services/browsing-files.service.ts | 34 ---- .../services/content-management.service.ts | 52 +++++ .../favorites/favorites.component.html | 173 +++++++++-------- src/app/components/files/files.component.html | 177 +++++++++--------- .../components/files/files.component.spec.ts | 14 -- src/app/components/files/files.component.ts | 7 +- .../components/layout/layout.component.html | 2 +- .../layout/layout.component.spec.ts | 25 --- src/app/components/layout/layout.component.ts | 27 ++- .../libraries/libraries.component.html | 46 ++--- src/app/components/page.component.ts | 16 +- .../recent-files/recent-files.component.html | 164 ++++++++-------- .../search-results.component.html | 105 ++++++----- .../shared-files/shared-files.component.html | 161 ++++++++-------- .../components/sidenav/sidenav.component.html | 17 +- .../sidenav/sidenav.component.spec.ts | 13 +- .../components/sidenav/sidenav.component.ts | 49 +++-- .../trashcan/trashcan.component.html | 51 ++--- .../components/trashcan/trashcan.component.ts | 5 +- src/app/directives/create-folder.directive.ts | 89 --------- src/app/directives/edit-folder.directive.ts | 49 +---- .../extensions/content-action.extension.ts | 2 +- src/app/extensions/extension.service.ts | 129 ++++++++++++- src/app/store/actions/app.actions.ts | 7 + src/app/store/actions/node.actions.ts | 13 ++ src/app/store/effects/node.effects.ts | 53 +++++- src/app/store/reducers/app.reducer.ts | 21 ++- src/app/store/selectors/app.selectors.ts | 1 + src/app/store/states/app.state.ts | 5 + .../states/navigation.state.ts} | 11 +- src/app/testing/app-testing.module.ts | 2 - 34 files changed, 842 insertions(+), 786 deletions(-) delete mode 100644 src/app/common/services/browsing-files.service.spec.ts delete mode 100644 src/app/common/services/browsing-files.service.ts delete mode 100644 src/app/directives/create-folder.directive.ts rename src/app/{extensions/create.extension.ts => store/states/navigation.state.ts} (87%) diff --git a/src/app.config.json b/src/app.config.json index a7b4d95cd9..372c1a2615 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -54,6 +54,17 @@ } ], "actions": [ + { + "id": "aca:actions/create-folder", + "type": "CREATE_FOLDER", + "payload": null + }, + { + "id": "aca:actions/edit-folder", + "type": "EDIT_FOLDER", + "payload": null + }, + { "id": "aca:actions/info", "type": "SNACKBAR_INFO", @@ -78,11 +89,15 @@ "features": { "create": [ { - "id": "aca:create/action1", + "disabled": false, + "id": "aca:create/folder", "order": 100, - "icon": "build", - "title": "Error", - "action": "aca:actions/error" + "icon": "create_new_folder", + "title": "ext: Create Folder", + "target": { + "permissions": ["create"], + "action": "aca:actions/create-folder" + } } ], "navigation": { @@ -175,28 +190,29 @@ "actions": [ { "disabled": false, - "id": "aca:action1", - "order": 100, - "title": "Info", - "icon": "build", + "id": "aca:toolbar/create-folder", + "order": 10, + "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", + "icon": "create_new_folder", "target": { - "type": "folder", - "permissions": ["one", "two"], - "action": "aca:actions/info" + "types": [], + "permissions": ["parent.create"], + "action": "aca:actions/create-folder" } }, { "disabled": false, - "id": "aca:action2", - "order": 101, - "title": "Node name", - "icon": "feedback", + "id": "aca:toolbar/edit-folder", + "order": 20, + "title": "APP.ACTIONS.EDIT", + "icon": "create", "target": { - "type": "folder", - "permissions": ["one", "two"], - "action": "aca:actions/node-name" + "types": ["folder"], + "permissions": ["update"], + "action": "aca:actions/edit-folder" } }, + { "disabled": false, "id": "aca:action3", @@ -204,8 +220,8 @@ "title": "Settings", "icon": "settings_applications", "target": { - "type": "folder", - "permissions": ["one", "two"], + "types": [], + "permissions": [], "action": "aca:actions/settings" } }, @@ -216,8 +232,8 @@ "title": "Error", "icon": "report_problem", "target": { - "type": "file", - "permissions": ["one", "two"], + "types": ["file"], + "permissions": ["update", "delete"], "action": "aca:actions/error" } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 888b18fec2..c3c5937ab1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -61,7 +61,6 @@ import { NodePermanentDeleteDirective } from './common/directives/node-permanent import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; import { NodeVersionsDirective } from './common/directives/node-versions.directive'; import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog'; -import { BrowsingFilesService } from './common/services/browsing-files.service'; import { ContentManagementService } from './common/services/content-management.service'; import { NodeActionsService } from './common/services/node-actions.service'; import { NodePermissionService } from './common/services/node-permission.service'; @@ -72,7 +71,6 @@ import { ExperimentalGuard } from './common/services/experimental-guard.service' import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component'; import { EditFolderDirective } from './directives/edit-folder.directive'; -import { CreateFolderDirective } from './directives/create-folder.directive'; import { DownloadNodesDirective } from './directives/download-nodes.directive'; import { AppStoreModule } from './store/app-store.module'; import { PaginationDirective } from './directives/pagination.directive'; @@ -136,7 +134,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro InfoDrawerComponent, SharedLinkViewComponent, EditFolderDirective, - CreateFolderDirective, DownloadNodesDirective, PaginationDirective, DocumentListDirective, @@ -152,7 +149,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro source: 'assets' } }, - BrowsingFilesService, ContentManagementService, NodeActionsService, NodePermissionService, diff --git a/src/app/common/services/browsing-files.service.spec.ts b/src/app/common/services/browsing-files.service.spec.ts deleted file mode 100644 index fea859cf59..0000000000 --- a/src/app/common/services/browsing-files.service.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*! - * @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 { BrowsingFilesService } from './browsing-files.service'; - -describe('BrowsingFilesService', () => { - let service: BrowsingFilesService; - - beforeEach(() => { - service = new BrowsingFilesService(); - }); - - it('subscribes to event', () => { - const value: any = 'test-value'; - - service.onChangeParent.subscribe((result) => { - expect(result).toBe(value); - }); - - service.onChangeParent.next(value); - }); -}); diff --git a/src/app/common/services/browsing-files.service.ts b/src/app/common/services/browsing-files.service.ts deleted file mode 100644 index e55f7e7230..0000000000 --- a/src/app/common/services/browsing-files.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * @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 { Subject } from 'rxjs/Rx'; -import { Injectable } from '@angular/core'; - -import { MinimalNodeEntryEntity } from 'alfresco-js-api'; - -@Injectable() -export class BrowsingFilesService { - onChangeParent = new Subject(); -} diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index 0d9e7284a5..4827297c28 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -25,6 +25,12 @@ import { Subject } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material'; +import { FolderDialogComponent } from '@alfresco/adf-content-services'; +import { SnackbarErrorAction } from '../../store/actions'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; @Injectable() export class ContentManagementService { @@ -36,4 +42,50 @@ export class ContentManagementService { folderCreated = new Subject(); siteDeleted = new Subject(); linksUnshared = new Subject(); + + constructor(private store: Store, private dialogRef: MatDialog) {} + + createFolder(parentNodeId: string) { + const dialogInstance = this.dialogRef.open(FolderDialogComponent, { + data: { + parentNodeId: parentNodeId, + createTitle: undefined, + nodeType: 'cm:folder' + }, + width: '400px' + }); + + dialogInstance.componentInstance.error.subscribe(message => { + this.store.dispatch(new SnackbarErrorAction(message)); + }); + + dialogInstance.afterClosed().subscribe(node => { + if (node) { + this.folderCreated.next(node); + } + }); + } + + editFolder(folder: MinimalNodeEntity) { + if (!folder) { + return; + } + + const dialog = this.dialogRef.open(FolderDialogComponent, { + data: { + folder: folder.entry + }, + width: '400px' + }); + + dialog.componentInstance.error.subscribe(message => { + this.store.dispatch(new SnackbarErrorAction(message)); + }); + + dialog.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { + if (node) { + this.folderEdited.next(node); + } + }); + } } diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 67b3c0cfe2..78a200c0e8 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -3,14 +3,17 @@ - - + + + + + - - - - - - - - - - - - - - + + + - + + + + + + + + + + + +
diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index 947e8da3bb..0335409336 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -6,14 +6,17 @@ (navigate)="onBreadcrumbNavigate($event)"> - - + + + + + - - - - - - - - - - - - - - - + + + - + + + + + + + + + + + + +
diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts index 07ddab203c..6783216917 100644 --- a/src/app/components/files/files.component.spec.ts +++ b/src/app/components/files/files.component.spec.ts @@ -33,7 +33,6 @@ import { } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; import { ContentManagementService } from '../../common/services/content-management.service'; -import { BrowsingFilesService } from '../../common/services/browsing-files.service'; import { NodeActionsService } from '../../common/services/node-actions.service'; import { FilesComponent } from './files.component'; import { AppTestingModule } from '../../testing/app-testing.module'; @@ -48,7 +47,6 @@ describe('FilesComponent', () => { let contentManagementService: ContentManagementService; let uploadService: UploadService; let router: Router; - let browsingFilesService: BrowsingFilesService; let nodeActionsService: NodeActionsService; let contentApi: ContentApiService; @@ -86,7 +84,6 @@ describe('FilesComponent', () => { contentManagementService = TestBed.get(ContentManagementService); uploadService = TestBed.get(UploadService); router = TestBed.get(Router); - browsingFilesService = TestBed.get(BrowsingFilesService); nodeActionsService = TestBed.get(NodeActionsService); contentApi = TestBed.get(ContentApiService); }); @@ -146,17 +143,6 @@ describe('FilesComponent', () => { expect(component.fetchNodes).toHaveBeenCalled(); }); - it('emits onChangeParent event', () => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); - spyOn(browsingFilesService.onChangeParent, 'next').and.callFake((val) => val); - - fixture.detectChanges(); - - expect(browsingFilesService.onChangeParent.next) - .toHaveBeenCalledWith(node); - }); - it('if should navigate to parent if node is not a folder', () => { node.isFolder = false; node.parentId = 'parent-id'; diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index ee74f40923..1b57e5fdbb 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -29,7 +29,6 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Rx'; -import { BrowsingFilesService } from '../../common/services/browsing-files.service'; import { ContentManagementService } from '../../common/services/content-management.service'; import { NodeActionsService } from '../../common/services/node-actions.service'; import { NodePermissionService } from '../../common/services/node-permission.service'; @@ -37,6 +36,7 @@ import { AppStore } from '../../store/states/app.state'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; +import { SetCurrentFolderAction } from '../../store/actions'; @Component({ templateUrl: './files.component.html' @@ -54,7 +54,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { private nodeActionsService: NodeActionsService, private uploadService: UploadService, private contentManagementService: ContentManagementService, - private browsingFilesService: BrowsingFilesService, public permission: NodePermissionService, extensions: ExtensionService) { super(store, extensions); @@ -103,7 +102,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { ngOnDestroy() { super.ngOnDestroy(); - this.browsingFilesService.onChangeParent.next(null); + this.store.dispatch(new SetCurrentFolderAction(null)); } fetchNodes(parentNodeId?: string): Observable { @@ -222,7 +221,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { } this.node = node; - this.browsingFilesService.onChangeParent.next(node); + this.store.dispatch(new SetCurrentFolderAction(node)); } // todo: review this approach once 5.2.3 is out diff --git a/src/app/components/layout/layout.component.html b/src/app/components/layout/layout.component.html index 52c37d768a..6f10394a1d 100644 --- a/src/app/components/layout/layout.component.html +++ b/src/app/components/layout/layout.component.html @@ -1,7 +1,7 @@
+ [disabled]="!canUpload"> { let fixture: ComponentFixture; let component: LayoutComponent; - let browsingFilesService: BrowsingFilesService; let appConfig: AppConfigService; let userPreference: UserPreferencesService; - const navItem = { - label: 'some-label', - route: { - url: '/some-url' - } - }; - beforeEach(() => { TestBed.configureTestingModule({ imports: [ AppTestingModule ], @@ -67,25 +57,10 @@ describe('LayoutComponent', () => { fixture = TestBed.createComponent(LayoutComponent); component = fixture.componentInstance; - browsingFilesService = TestBed.get(BrowsingFilesService); appConfig = TestBed.get(AppConfigService); userPreference = TestBed.get(UserPreferencesService); }); - it('sets current node', () => { - appConfig.config = { - navigation: [navItem] - }; - - const currentNode = { id: 'someId' }; - - fixture.detectChanges(); - - browsingFilesService.onChangeParent.next(currentNode); - - expect(component.node).toEqual(currentNode); - }); - describe('sidenav state', () => { it('should get state from configuration', () => { appConfig.config = { diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index 4f1cf43bcb..3f6da924f1 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -24,11 +24,14 @@ */ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; -import { Subscription } from 'rxjs/Rx'; +import { Subject } from 'rxjs/Rx'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { BrowsingFilesService } from '../../common/services/browsing-files.service'; import { NodePermissionService } from '../../common/services/node-permission.service'; import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { currentFolder } from '../../store/selectors/app.selectors'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-layout', @@ -38,14 +41,14 @@ import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive' export class LayoutComponent implements OnInit, OnDestroy { @ViewChild(SidenavViewsManagerDirective) manager: SidenavViewsManagerDirective; + onDestroy$: Subject = new Subject(); expandedSidenav: boolean; node: MinimalNodeEntryEntity; - - private subscriptions: Subscription[] = []; + canUpload = false; constructor( - private browsingFilesService: BrowsingFilesService, - public permission: NodePermissionService) {} + protected store: Store, + private permission: NodePermissionService) {} ngOnInit() { if (!this.manager.minimizeSidenav) { @@ -56,12 +59,16 @@ export class LayoutComponent implements OnInit, OnDestroy { this.manager.run(true); - this.subscriptions.concat([ - this.browsingFilesService.onChangeParent.subscribe((node: MinimalNodeEntryEntity) => this.node = node) - ]); + this.store.select(currentFolder) + .pipe(takeUntil(this.onDestroy$)) + .subscribe(node => { + this.node = node; + this.canUpload = this.permission.check(node, ['create']); + }); } ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()); + this.onDestroy$.next(true); + this.onDestroy$.complete(); } } diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index f46f6a0d1a..85900e602f 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -3,29 +3,33 @@ - - - - - + + + + + - + + + +
diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index d883f9b501..f2113c5742 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -73,22 +73,8 @@ export abstract class PageComponent implements OnInit, OnDestroy { this.selection = selection; if (selection.isEmpty) { this.infoDrawerOpened = false; - this.actions = []; - } else { - this.actions = this.extensions.contentActions.filter(action => { - if (action.target && action.target.type) { - switch (action.target.type.toLowerCase()) { - case 'folder': - return selection.folder ? true : false; - case 'file': - return selection.file ? true : false; - default: - return false; - } - } - return false; - }); } + this.actions = this.extensions.getSelectedContentActions(selection, this.node); }); } diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index aba56b74cd..5f57db1817 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -3,14 +3,17 @@ - - + + + + + - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index e80d449c38..6041de430c 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -2,8 +2,9 @@
- + + + - - - - - - - - - + + + + - + + + + + + + + +
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 79ad47d639..6480193424 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -3,14 +3,17 @@ - - + + + + + - - - - - - - - - - + - - - + + + + + + + + + + + + + + diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index dd1e53610b..851a0b6b4e 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -9,17 +9,18 @@ - + + + + + - - + + + + + @@ -97,7 +104,7 @@ diff --git a/src/app/components/trashcan/trashcan.component.ts b/src/app/components/trashcan/trashcan.component.ts index f02a03dbf8..f9e943342b 100644 --- a/src/app/components/trashcan/trashcan.component.ts +++ b/src/app/components/trashcan/trashcan.component.ts @@ -31,17 +31,19 @@ import { selectUser } from '../../store/selectors/app.selectors'; import { AppStore } from '../../store/states/app.state'; import { ProfileState } from '../../store/states/profile.state'; import { ExtensionService } from '../../extensions/extension.service'; +import { Observable } from 'rxjs/Observable'; @Component({ templateUrl: './trashcan.component.html' }) export class TrashcanComponent extends PageComponent implements OnInit { - user: ProfileState; + user$: Observable; constructor(private contentManagementService: ContentManagementService, extensions: ExtensionService, store: Store) { super(store, extensions); + this.user$ = this.store.select(selectUser); } ngOnInit() { @@ -51,7 +53,6 @@ export class TrashcanComponent extends PageComponent implements OnInit { this.contentManagementService.nodesRestored.subscribe(() => this.reload()), this.contentManagementService.nodesPurged.subscribe(() => this.reload()), this.contentManagementService.nodesRestored.subscribe(() => this.reload()), - this.store.select(selectUser).subscribe((user) => this.user = user) ); } } diff --git a/src/app/directives/create-folder.directive.ts b/src/app/directives/create-folder.directive.ts deleted file mode 100644 index 4dcb190159..0000000000 --- a/src/app/directives/create-folder.directive.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * @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 { MatDialog, MatDialogConfig } from '@angular/material'; -import { FolderDialogComponent } from '@alfresco/adf-content-services'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { SnackbarErrorAction } from '../store/actions'; -import { ContentManagementService } from '../common/services/content-management.service'; - -@Directive({ - selector: '[acaCreateFolder]' -}) -export class CreateFolderDirective { - /** Parent folder where the new folder will be located after creation. */ - // tslint:disable-next-line:no-input-rename - @Input('acaCreateFolder') parentNodeId: string; - - /** Title of folder creation dialog. */ - @Input() dialogTitle: string = null; - - /** Type of node to create. */ - @Input() nodeType = 'cm:folder'; - - @HostListener('click', ['$event']) - onClick(event: Event) { - if (this.parentNodeId) { - event.preventDefault(); - this.openDialog(); - } - } - - constructor( - private store: Store, - private dialogRef: MatDialog, - private content: ContentManagementService - ) {} - - private get dialogConfig(): MatDialogConfig { - return { - data: { - parentNodeId: this.parentNodeId, - createTitle: this.dialogTitle, - nodeType: this.nodeType - }, - width: '400px' - }; - } - - private openDialog(): void { - const dialogInstance = this.dialogRef.open( - FolderDialogComponent, - this.dialogConfig - ); - - dialogInstance.componentInstance.error.subscribe(message => { - this.store.dispatch(new SnackbarErrorAction(message)); - }); - - dialogInstance.afterClosed().subscribe(node => { - if (node) { - this.content.folderCreated.next(node); - } - }); - } -} diff --git a/src/app/directives/edit-folder.directive.ts b/src/app/directives/edit-folder.directive.ts index e532ec87d3..650e2c3bc0 100644 --- a/src/app/directives/edit-folder.directive.ts +++ b/src/app/directives/edit-folder.directive.ts @@ -24,59 +24,24 @@ */ import { Directive, Input, HostListener } from '@angular/core'; -import { MinimalNodeEntryEntity, MinimalNodeEntity } from 'alfresco-js-api'; -import { MatDialog, MatDialogConfig } from '@angular/material'; -import { FolderDialogComponent } from '@alfresco/adf-content-services'; +import { MinimalNodeEntity } from 'alfresco-js-api'; import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { SnackbarErrorAction } from '../store/actions'; -import { ContentManagementService } from '../common/services/content-management.service'; +import { AppStore } from '../store/states'; +import { EditFolderAction } from '../store/actions'; @Directive({ selector: '[acaEditFolder]' }) export class EditFolderDirective { - /** Folder node to edit. */ // tslint:disable-next-line:no-input-rename - @Input('acaEditFolder') - folder: MinimalNodeEntity; + @Input('acaEditFolder') folder: MinimalNodeEntity; - @HostListener('click', [ '$event' ]) + @HostListener('click', ['$event']) onClick(event) { event.preventDefault(); - - if (this.folder) { - this.openDialog(); - } - } - - constructor( - private store: Store, - private dialogRef: MatDialog, - private content: ContentManagementService - ) {} - - private get dialogConfig(): MatDialogConfig { - return { - data: { - folder: this.folder.entry - }, - width: '400px' - }; + this.store.dispatch(new EditFolderAction(this.folder)); } - private openDialog(): void { - const dialog = this.dialogRef.open(FolderDialogComponent, this.dialogConfig); - - dialog.componentInstance.error.subscribe(message => { - this.store.dispatch(new SnackbarErrorAction(message)); - }); - - dialog.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { - if (node) { - this.content.folderEdited.next(node); - } - }); - } + constructor(private store: Store) {} } diff --git a/src/app/extensions/content-action.extension.ts b/src/app/extensions/content-action.extension.ts index c11158f76f..2209dd4acd 100644 --- a/src/app/extensions/content-action.extension.ts +++ b/src/app/extensions/content-action.extension.ts @@ -30,7 +30,7 @@ export interface ContentActionExtension { icon?: string; disabled?: boolean; target: { - type: string; + types: Array; permissions: Array, action: string; }; diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 1f59e862c7..3463a646a9 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -29,11 +29,11 @@ import { ActionExtension } from './action.extension'; import { AppConfigService } from '@alfresco/adf-core'; import { ContentActionExtension } from './content-action.extension'; import { OpenWithExtension } from './open-with.extension'; -import { AppStore } from '../store/states'; +import { AppStore, SelectionState } from '../store/states'; import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; -import { CreateExtension } from './create.extension'; +import { Node } from 'alfresco-js-api'; @Injectable() export class ExtensionService { @@ -42,7 +42,7 @@ export class ExtensionService { contentActions: Array = []; openWithActions: Array = []; - createActions: Array = []; + createActions: Array = []; authGuards: { [key: string]: Type<{}> } = {}; components: { [key: string]: Type<{}> } = {}; @@ -70,7 +70,6 @@ export class ExtensionService { 'extensions.core.features.content.actions', [] ) - .filter(entry => !entry.disabled) .sort(this.sortByOrder); this.openWithActions = this.config @@ -82,8 +81,10 @@ export class ExtensionService { .sort(this.sortByOrder); this.createActions = this.config - .get>('extensions.core.features.create', []) - .filter(entry => !entry.disabled) + .get>( + 'extensions.core.features.create', + [] + ) .sort(this.sortByOrder); } @@ -170,11 +171,48 @@ export class ExtensionService { component: this.getComponentById(route.component), data: route.data } - ], + ] }; }); } + // evaluates create actions for the folder node + getFolderCreateActions(folder: Node): Array { + return this.createActions.filter(this.filterOutDisabled).map(action => { + if ( + action.target && + action.target.permissions && + action.target.permissions.length > 0 + ) { + return { + ...action, + disabled: !this.nodeHasPermissions( + folder, + action.target.permissions + ), + target: { + ...action.target + } + }; + } + return action; + }); + } + + // evaluates content actions for the selection and parent folder node + getSelectedContentActions( + selection: SelectionState, + parentNode: Node + ): Array { + return this.contentActions + .filter(this.filterOutDisabled) + .filter(action => action.target) + .filter(action => this.filterByTarget(selection, action)) + .filter(action => + this.filterByPermission(selection, action, parentNode) + ); + } + private sortByOrder( a: { order?: number | undefined }, b: { order?: number | undefined } @@ -183,4 +221,81 @@ export class ExtensionService { const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; return left - right; } + + private filterOutDisabled(entry: { disabled?: boolean }): boolean { + return !entry.disabled; + } + + // todo: support multiple selected nodes + private filterByTarget( + selection: SelectionState, + action: ContentActionExtension + ): boolean { + const types = action.target.types; + + if (!types || types.length === 0) { + return true; + } + + if (selection && selection.folder && types.includes('folder')) { + return true; + } + + if (selection && selection.file && types.includes('file')) { + return true; + } + + return false; + } + + // todo: support multiple selected nodes + private filterByPermission( + selection: SelectionState, + action: ContentActionExtension, + parentNode: Node + ): boolean { + const permissions = action.target.permissions; + + if (!permissions || permissions.length === 0) { + return true; + } + + return permissions.some(permission => { + if (permission.startsWith('parent.')) { + if (parentNode) { + const parentQuery = permission.split('.')[1]; + // console.log(parentNode.allowableOperations, parentQuery); + return this.nodeHasPermissions(parentNode, [parentQuery]); + } + return false; + } + + if (selection && selection.first) { + return this.nodeHasPermissions( + selection.first.entry, + permissions + ); + } + + return true; + }); + + return true; + } + + private nodeHasPermissions( + node: Node, + permissions: string[] = [] + ): boolean { + if ( + node && + node.allowableOperations && + node.allowableOperations.length > 0 + ) { + return permissions.some(permission => + node.allowableOperations.includes(permission) + ); + } + return false; + } } diff --git a/src/app/store/actions/app.actions.ts b/src/app/store/actions/app.actions.ts index 2cfce276d4..9c8c2dd5a7 100644 --- a/src/app/store/actions/app.actions.ts +++ b/src/app/store/actions/app.actions.ts @@ -24,12 +24,14 @@ */ import { Action } from '@ngrx/store'; +import { Node } from 'alfresco-js-api'; export const SET_APP_NAME = 'SET_APP_NAME'; export const SET_HEADER_COLOR = 'SET_HEADER_COLOR'; export const SET_LOGO_PATH = 'SET_LOGO_PATH'; export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER'; export const SET_SHARED_URL = 'SET_SHARED_URL'; +export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER'; export class SetAppNameAction implements Action { readonly type = SET_APP_NAME; @@ -55,3 +57,8 @@ export class SetSharedUrlAction implements Action { readonly type = SET_SHARED_URL; constructor(public payload: string) {} } + +export class SetCurrentFolderAction implements Action { + readonly type = SET_CURRENT_FOLDER; + constructor(public payload: Node) {} +} diff --git a/src/app/store/actions/node.actions.ts b/src/app/store/actions/node.actions.ts index c8b1a90631..04a57c3aa8 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -25,6 +25,7 @@ import { Action } from '@ngrx/store'; import { NodeInfo } from '../models'; +import { MinimalNodeEntity } from 'alfresco-js-api'; export const SET_SELECTED_NODES = 'SET_SELECTED_NODES'; export const DELETE_NODES = 'DELETE_NODES'; @@ -32,6 +33,8 @@ 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 const CREATE_FOLDER = 'CREATE_FOLDER'; +export const EDIT_FOLDER = 'EDIT_FOLDER'; export class SetSelectedNodesAction implements Action { readonly type = SET_SELECTED_NODES; @@ -62,3 +65,13 @@ export class DownloadNodesAction implements Action { readonly type = DOWNLOAD_NODES; constructor(public payload: NodeInfo[] = []) {} } + +export class CreateFolderAction implements Action { + readonly type = CREATE_FOLDER; + constructor(public payload: string) {} +} + +export class EditFolderAction implements Action { + readonly type = EDIT_FOLDER; + constructor(public payload: MinimalNodeEntity) {} +} diff --git a/src/app/store/effects/node.effects.ts b/src/app/store/effects/node.effects.ts index c0adad2a33..cf8eb91856 100644 --- a/src/app/store/effects/node.effects.ts +++ b/src/app/store/effects/node.effects.ts @@ -39,12 +39,16 @@ import { SnackbarUserAction, SnackbarAction, UndoDeleteNodesAction, - UNDO_DELETE_NODES + UNDO_DELETE_NODES, + CreateFolderAction, + CREATE_FOLDER } from '../actions'; import { ContentManagementService } from '../../common/services/content-management.service'; import { Observable } from 'rxjs/Rx'; import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models'; import { ContentApiService } from '../../services/content-api.service'; +import { currentFolder, appSelection } from '../selectors/app.selectors'; +import { EditFolderAction, EDIT_FOLDER } from '../actions/node.actions'; @Injectable() export class NodeEffects { @@ -83,6 +87,44 @@ export class NodeEffects { }) ); + @Effect({ dispatch: false }) + createFolder$ = this.actions$.pipe( + ofType(CREATE_FOLDER), + map(action => { + if (action.payload) { + this.contentManagementService.createFolder(action.payload); + } else { + this.store + .select(currentFolder) + .take(1) + .subscribe(node => { + if (node && node.id) { + this.contentManagementService.createFolder(node.id); + } + }); + } + }) + ); + + @Effect({ dispatch: false }) + editFolder$ = this.actions$.pipe( + ofType(EDIT_FOLDER), + map(action => { + if (action.payload) { + this.contentManagementService.editFolder(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && selection.folder) { + this.contentManagementService.editFolder(selection.folder); + } + }); + } + }) + ); + private deleteNodes(items: NodeInfo[]): void { const batch: Observable[] = []; @@ -113,7 +155,8 @@ export class NodeEffects { private deleteNode(node: NodeInfo): Observable { const { id, name } = node; - return this.contentApi.deleteNode(id) + return this.contentApi + .deleteNode(id) .map(() => { return { id, @@ -206,7 +249,8 @@ export class NodeEffects { private undoDeleteNode(item: DeletedNodeInfo): Observable { const { id, name } = item; - return this.contentApi.restoreNode(id) + return this.contentApi + .restoreNode(id) .map(() => { return { id, @@ -263,7 +307,8 @@ export class NodeEffects { private purgeDeletedNode(node: NodeInfo): Observable { const { id, name } = node; - return this.contentApi.purgeDeletedNode(id) + return this.contentApi + .purgeDeletedNode(id) .map(() => ({ status: 1, id, diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts index b77d9e18bc..caa6b73775 100644 --- a/src/app/store/reducers/app.reducer.ts +++ b/src/app/store/reducers/app.reducer.ts @@ -35,14 +35,14 @@ import { SET_SELECTED_NODES, SetSelectedNodesAction, SET_USER, - SetUserAction -} from '../actions'; -import { + SetUserAction, SET_LANGUAGE_PICKER, SetLanguagePickerAction, SET_SHARED_URL, - SetSharedUrlAction -} from '../actions/app.actions'; + SetSharedUrlAction, + SET_CURRENT_FOLDER +} from '../actions'; +import { SetCurrentFolderAction } from '../actions/app.actions'; export function appReducer( state: AppState = INITIAL_APP_STATE, @@ -74,7 +74,10 @@ export function appReducer( )); break; case SET_SHARED_URL: - newState = updateSharedUrl(state, ( + newState = updateSharedUrl(state, action); + break; + case SET_CURRENT_FOLDER: + newState = updateCurrentFolder(state, ( action )); break; @@ -149,6 +152,12 @@ function updateUser(state: AppState, action: SetUserAction): AppState { return newState; } +function updateCurrentFolder(state: AppState, action: SetCurrentFolderAction) { + const newState = Object.assign({}, state); + newState.navigation.currentFolder = action.payload; + return newState; +} + function updateSelectedNodes( state: AppState, action: SetSelectedNodesAction diff --git a/src/app/store/selectors/app.selectors.ts b/src/app/store/selectors/app.selectors.ts index e5196364a9..49277c9d80 100644 --- a/src/app/store/selectors/app.selectors.ts +++ b/src/app/store/selectors/app.selectors.ts @@ -34,3 +34,4 @@ export const appSelection = createSelector(selectApp, state => state.selection) export const appLanguagePicker = createSelector(selectApp, state => state.languagePicker); export const selectUser = createSelector(selectApp, state => state.user); export const sharedUrl = createSelector(selectApp, state => state.sharedUrl); +export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder); diff --git a/src/app/store/states/app.state.ts b/src/app/store/states/app.state.ts index ca5b4eb0b5..2b134d029f 100644 --- a/src/app/store/states/app.state.ts +++ b/src/app/store/states/app.state.ts @@ -25,6 +25,7 @@ import { SelectionState } from './selection.state'; import { ProfileState } from './profile.state'; +import { NavigationState } from './navigation.state'; export interface AppState { appName: string; @@ -34,6 +35,7 @@ export interface AppState { sharedUrl: string; selection: SelectionState; user: ProfileState; + navigation: NavigationState; } export const INITIAL_APP_STATE: AppState = { @@ -52,6 +54,9 @@ export const INITIAL_APP_STATE: AppState = { nodes: [], isEmpty: true, count: 0 + }, + navigation: { + currentFolder: null } }; diff --git a/src/app/extensions/create.extension.ts b/src/app/store/states/navigation.state.ts similarity index 87% rename from src/app/extensions/create.extension.ts rename to src/app/store/states/navigation.state.ts index 8393f9758f..dd41681fec 100644 --- a/src/app/extensions/create.extension.ts +++ b/src/app/store/states/navigation.state.ts @@ -23,11 +23,8 @@ * along with Alfresco. If not, see . */ -export interface CreateExtension { - id: string; - order?: number; - title: string; - icon?: string; - action: string; - disabled?: boolean; +import { Node } from 'alfresco-js-api'; + +export interface NavigationState { + currentFolder?: Node; } diff --git a/src/app/testing/app-testing.module.ts b/src/app/testing/app-testing.module.ts index 08f47e7747..81472d3b1e 100644 --- a/src/app/testing/app-testing.module.ts +++ b/src/app/testing/app-testing.module.ts @@ -60,7 +60,6 @@ import { MaterialModule } from '../material.module'; import { ContentManagementService } from '../common/services/content-management.service'; import { NodeActionsService } from '../common/services/node-actions.service'; import { NodePermissionService } from '../common/services/node-permission.service'; -import { BrowsingFilesService } from '../common/services/browsing-files.service'; import { ContentApiService } from '../services/content-api.service'; import { ExtensionService } from '../extensions/extension.service'; @@ -116,7 +115,6 @@ import { ExtensionService } from '../extensions/extension.service'; ContentManagementService, NodeActionsService, NodePermissionService, - BrowsingFilesService, ContentApiService, ExtensionService ] From 718a32a9073fd13ffbbcb6443af31c723aa37d89 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Sun, 8 Jul 2018 12:25:20 +0100 Subject: [PATCH 013/146] [ACA-1529] performance fixes for permission checks (#498) * fix recent files * fix files component * fix shared files * don't evaluate permissions for empty selection * fix info drawer * fix viewer * fix tests * reduce one more check * track upload errors on app level * remove console log * reduce service dependencies --- src/app/app.component.ts | 58 +++++++++++++------ .../services/content-management.service.ts | 41 ++++++++++++- .../favorites/favorites.component.ts | 6 +- src/app/components/files/files.component.html | 8 +-- src/app/components/files/files.component.ts | 19 +++--- .../header/header.component.spec.ts | 8 +-- .../info-drawer/info-drawer.component.html | 4 +- .../info-drawer/info-drawer.component.ts | 19 +++--- .../layout/layout.component.spec.ts | 11 +--- src/app/components/layout/layout.component.ts | 2 +- .../libraries/libraries.component.ts | 4 +- src/app/components/page.component.spec.ts | 2 +- src/app/components/page.component.ts | 41 ++++++------- .../components/preview/preview.component.html | 10 ++-- .../components/preview/preview.component.ts | 20 +++---- .../recent-files/recent-files.component.html | 4 +- .../recent-files/recent-files.component.ts | 14 ++--- .../search-results.component.ts | 9 +-- .../shared-files/shared-files.component.html | 8 +-- .../shared-files/shared-files.component.ts | 12 +--- .../components/sidenav/sidenav.component.ts | 2 +- .../components/trashcan/trashcan.component.ts | 10 ++-- src/app/testing/app-testing.module.ts | 4 -- 23 files changed, 168 insertions(+), 148 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4f1b39f2da..963f6ce14f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -23,21 +23,27 @@ * along with Alfresco. If not, see . */ -import { Component, OnInit } from '@angular/core'; -import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; import { - PageTitleService, AppConfigService, - AuthenticationService, AlfrescoApiService } from '@alfresco/adf-core'; + AlfrescoApiService, + AppConfigService, + AuthenticationService, + FileUploadErrorEvent, + PageTitleService, + UploadService +} from '@alfresco/adf-core'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { AppStore } from './store/states/app.state'; +import { ExtensionService } from './extensions/extension.service'; import { - SetHeaderColorAction, SetAppNameAction, - SetLogoPathAction, + SetHeaderColorAction, SetLanguagePickerAction, - SetSharedUrlAction + SetLogoPathAction, + SetSharedUrlAction, + SnackbarErrorAction } from './store/actions'; -import { ExtensionService } from './extensions/extension.service'; +import { AppStore } from './store/states/app.state'; @Component({ selector: 'app-root', @@ -53,11 +59,12 @@ export class AppComponent implements OnInit { private config: AppConfigService, private alfrescoApiService: AlfrescoApiService, private authenticationService: AuthenticationService, - private extensions: ExtensionService) { - } + private uploadService: UploadService, + private extensions: ExtensionService + ) {} ngOnInit() { - this.alfrescoApiService.getInstance().on('error', (error) => { + this.alfrescoApiService.getInstance().on('error', error => { if (error.status === 401) { if (!this.authenticationService.isLoggedIn()) { this.router.navigate(['/login']); @@ -65,13 +72,11 @@ export class AppComponent implements OnInit { } }); - this.loadAppSettings(); const { router, pageTitle, route } = this; - router - .events + router.events .filter(event => event instanceof NavigationEnd) .subscribe(() => { let currentRoute = route.root; @@ -88,8 +93,10 @@ export class AppComponent implements OnInit { this.extensions.init(); - this.router.config.unshift( - ...this.extensions.getApplicationRoutes() + this.router.config.unshift(...this.extensions.getApplicationRoutes()); + + this.uploadService.fileUploadError.subscribe(error => + this.onFileUploadedError(error) ); } @@ -109,7 +116,22 @@ export class AppComponent implements OnInit { const languagePicker = this.config.get('languagePicker'); this.store.dispatch(new SetLanguagePickerAction(languagePicker)); - const sharedPreviewUrl = this.config.get('ecmHost') + '/#/preview/s/'; + const sharedPreviewUrl = + this.config.get('ecmHost') + '/#/preview/s/'; this.store.dispatch(new SetSharedUrlAction(sharedPreviewUrl)); } + + onFileUploadedError(error: FileUploadErrorEvent) { + let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC'; + + if (error.error.status === 409) { + message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT'; + } + + if (error.error.status === 500) { + message = 'APP.MESSAGES.UPLOAD.ERROR.500'; + } + + this.store.dispatch(new SnackbarErrorAction(message)); + } } diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index 4827297c28..fd5314106e 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -30,7 +30,12 @@ import { FolderDialogComponent } from '@alfresco/adf-content-services'; import { SnackbarErrorAction } from '../../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states'; -import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { + MinimalNodeEntity, + MinimalNodeEntryEntity, + Node +} from 'alfresco-js-api'; +import { NodePermissionService } from './node-permission.service'; @Injectable() export class ContentManagementService { @@ -43,7 +48,11 @@ export class ContentManagementService { siteDeleted = new Subject(); linksUnshared = new Subject(); - constructor(private store: Store, private dialogRef: MatDialog) {} + constructor( + private store: Store, + private permission: NodePermissionService, + private dialogRef: MatDialog + ) {} createFolder(parentNodeId: string) { const dialogInstance = this.dialogRef.open(FolderDialogComponent, { @@ -88,4 +97,32 @@ export class ContentManagementService { } }); } + + canDeleteNode(node: MinimalNodeEntity | Node): boolean { + return this.permission.check(node, ['delete']); + } + + canDeleteNodes(nodes: MinimalNodeEntity[]): boolean { + return this.permission.check(nodes, ['delete']); + } + + canUpdateNode(node: MinimalNodeEntity | Node): boolean { + return this.permission.check(node, ['update']); + } + + canUploadContent(folderNode: MinimalNodeEntity | Node): boolean { + return this.permission.check(folderNode, ['create']); + } + + canDeleteSharedNodes(sharedLinks: MinimalNodeEntity[]): boolean { + return this.permission.check(sharedLinks, ['delete'], { + target: 'allowableOperationsOnTarget' + }); + } + + canUpdateSharedNode(sharedLink: MinimalNodeEntity): boolean { + return this.permission.check(sharedLink, ['update'], { + target: 'allowableOperationsOnTarget' + }); + } } diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts index 59fc669729..3631341431 100644 --- a/src/app/components/favorites/favorites.component.ts +++ b/src/app/components/favorites/favorites.component.ts @@ -33,7 +33,6 @@ import { PathInfo } from 'alfresco-js-api'; import { ContentManagementService } from '../../common/services/content-management.service'; -import { NodePermissionService } from '../../common/services/node-permission.service'; import { AppStore } from '../../store/states/app.state'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; @@ -48,10 +47,9 @@ export class FavoritesComponent extends PageComponent implements OnInit { store: Store, extensions: ExtensionService, private contentApi: ContentApiService, - private content: ContentManagementService, - public permission: NodePermissionService + content: ContentManagementService ) { - super(store, extensions); + super(store, extensions, content); } ngOnInit() { diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index 0335409336..4af65c28bf 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -48,7 +48,7 @@ @@ -67,22 +47,22 @@ diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index 2fe5d3bb8b..6d0816f10f 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -26,10 +26,9 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_OUTLET } from '@angular/router'; import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core'; -import { Node, MinimalNodeEntity } from 'alfresco-js-api'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; -import { DeleteNodesAction } from '../../store/actions'; +import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; @@ -44,24 +43,17 @@ import { ContentManagementService } from '../../common/services/content-manageme }) export class PreviewComponent extends PageComponent implements OnInit { - node: Node; previewLocation: string = null; routesSkipNavigation = [ 'shared', 'recent-files', 'favorites' ]; navigateSource: string = null; navigationSources = ['favorites', 'libraries', 'personal-files', 'recent-files', 'shared']; - folderId: string = null; nodeId: string = null; previousNodeId: string; nextNodeId: string; navigateMultiple = false; - - selectedEntities: MinimalNodeEntity[] = []; openWith: Array = []; - canDeletePreview = false; - canUpdatePreview = false; - constructor( private contentApi: ContentApiService, private preferences: UserPreferencesService, @@ -74,6 +66,8 @@ export class PreviewComponent extends PageComponent implements OnInit { } ngOnInit() { + super.ngOnInit(); + this.previewLocation = this.router.url .substr(0, this.router.url.indexOf('/', 1)) .replace(/\//g, ''); @@ -110,9 +104,7 @@ export class PreviewComponent extends PageComponent implements OnInit { if (id) { try { this.node = await this.contentApi.getNodeInfo(id).toPromise(); - this.selectedEntities = [{ entry: this.node }]; - this.canDeletePreview = this.node && this.content.canDeleteNode(this.node); - this.canUpdatePreview = this.node && this.content.canUpdateNode(this.node); + this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }])); if (this.node && this.node.isFile) { const nearest = await this.getNearestNodes(this.node.id, this.node.parentId); @@ -364,10 +356,4 @@ export class PreviewComponent extends PageComponent implements OnInit { return acc; }, []); } - - // this is where each application decides how to treat an action and what to do - // the ACA maps actions to the NgRx actions as an example - runAction(actionId: string) { - this.extensions.runActionById(actionId); - } } diff --git a/src/app/store/actions/node.actions.ts b/src/app/store/actions/node.actions.ts index 04a57c3aa8..6a8b543801 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -38,7 +38,7 @@ export const EDIT_FOLDER = 'EDIT_FOLDER'; export class SetSelectedNodesAction implements Action { readonly type = SET_SELECTED_NODES; - constructor(public payload: any[] = []) {} + constructor(public payload: MinimalNodeEntity[] = []) {} } export class DeleteNodesAction implements Action { diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts index caa6b73775..208bca31c3 100644 --- a/src/app/store/reducers/app.reducer.ts +++ b/src/app/store/reducers/app.reducer.ts @@ -179,7 +179,7 @@ function updateSelectedNodes( if (nodes.length === 1) { file = nodes.find(entity => { // workaround Shared - return entity.entry.isFile || entity.entry.nodeId; + return (entity.entry.isFile || entity.entry.nodeId) ? true : false; }); folder = nodes.find(entity => entity.entry.isFolder); } From 1c48198e7944339306fbec519f14b141eee03f99 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Sun, 8 Jul 2018 18:40:36 +0100 Subject: [PATCH 015/146] [ACA-1530] allow calling download action from extensions (#500) * download action * use download action everywhere --- src/app.config.json | 20 ++++++- src/app/app.module.ts | 2 - .../favorites/favorites.component.html | 2 +- src/app/components/files/files.component.html | 2 +- src/app/components/page.component.ts | 6 +- .../recent-files/recent-files.component.html | 2 +- .../search-results.component.html | 2 +- .../shared-files/shared-files.component.html | 2 +- .../directives/download-nodes.directive.ts | 58 ------------------- src/app/store/actions/node.actions.ts | 2 +- src/app/store/effects/download.effects.ts | 39 ++++++++++--- 11 files changed, 62 insertions(+), 75 deletions(-) delete mode 100644 src/app/directives/download-nodes.directive.ts diff --git a/src/app.config.json b/src/app.config.json index 372c1a2615..a63abcfcc8 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -64,6 +64,11 @@ "type": "EDIT_FOLDER", "payload": null }, + { + "id": "aca:actions/download", + "type": "DOWNLOAD_NODES", + "payload": null + }, { "id": "aca:actions/info", @@ -202,8 +207,20 @@ }, { "disabled": false, - "id": "aca:toolbar/edit-folder", + "id": "aca:toolbar/download", "order": 20, + "title": "APP.ACTIONS.DOWNLOAD", + "icon": "get_app", + "target": { + "types": ["folder", "file"], + "permissions": [], + "action": "aca:actions/download" + } + }, + { + "disabled": false, + "id": "aca:toolbar/edit-folder", + "order": 30, "title": "APP.ACTIONS.EDIT", "icon": "create", "target": { @@ -213,6 +230,7 @@ } }, + { "disabled": false, "id": "aca:action3", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c3c5937ab1..81316b2f14 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -71,7 +71,6 @@ import { ExperimentalGuard } from './common/services/experimental-guard.service' import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component'; import { EditFolderDirective } from './directives/edit-folder.directive'; -import { DownloadNodesDirective } from './directives/download-nodes.directive'; import { AppStoreModule } from './store/app-store.module'; import { PaginationDirective } from './directives/pagination.directive'; import { DocumentListDirective } from './directives/document-list.directive'; @@ -134,7 +133,6 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro InfoDrawerComponent, SharedLinkViewComponent, EditFolderDirective, - DownloadNodesDirective, PaginationDirective, DocumentListDirective, ExperimentalDirective diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 78a200c0e8..975c562aff 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -38,7 +38,7 @@ mat-icon-button color="primary" title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" - [adfNodeDownload]="selection.nodes"> + (click)="downloadSelection()"> get_app diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index 4af65c28bf..a718f2bfef 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -41,7 +41,7 @@ color="primary" mat-icon-button title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" - [adfNodeDownload]="selection.nodes"> + (click)="downloadSelection()"> get_app diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 363f1c769e..0bc69dca64 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -30,7 +30,7 @@ import { Store } from '@ngrx/store'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; import { takeUntil } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs/Rx'; -import { ViewNodeAction, SetSelectedNodesAction } from '../store/actions'; +import { ViewNodeAction, SetSelectedNodesAction, DownloadNodesAction } from '../store/actions'; import { appSelection, sharedUrl, currentFolder } from '../store/selectors/app.selectors'; import { AppStore } from '../store/states/app.state'; import { SelectionState } from '../store/states/selection.state'; @@ -154,6 +154,10 @@ export abstract class PageComponent implements OnInit, OnDestroy { this.documentList.display = this.displayMode; } + downloadSelection() { + this.store.dispatch(new DownloadNodesAction()); + } + // this is where each application decides how to treat an action and what to do // the ACA maps actions to the NgRx actions as an example runAction(actionId: string) { diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index 8996f46934..9b64939449 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -38,7 +38,7 @@ mat-icon-button color="primary" title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" - [adfNodeDownload]="selection.nodes"> + (click)="downloadSelection()"> get_app diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index 6041de430c..ac2eeb0ce4 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -29,7 +29,7 @@ color="primary" mat-icon-button title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" - [acaDownloadNodes]="selection.nodes"> + (click)="downloadSelection()"> get_app diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 001c4da8d8..91c196ec95 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -38,7 +38,7 @@ color="primary" mat-icon-button title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}" - [adfNodeDownload]="selection.nodes"> + (click)="downloadSelection()"> get_app diff --git a/src/app/directives/download-nodes.directive.ts b/src/app/directives/download-nodes.directive.ts deleted file mode 100644 index ea2d61dc71..0000000000 --- a/src/app/directives/download-nodes.directive.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*! - * @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-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.actions.ts b/src/app/store/actions/node.actions.ts index 6a8b543801..958a5fd443 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -63,7 +63,7 @@ export class PurgeDeletedNodesAction implements Action { export class DownloadNodesAction implements Action { readonly type = DOWNLOAD_NODES; - constructor(public payload: NodeInfo[] = []) {} + constructor(public payload: MinimalNodeEntity[] = []) {} } export class CreateFolderAction implements Action { diff --git a/src/app/store/effects/download.effects.ts b/src/app/store/effects/download.effects.ts index 72a6d1dbb2..fecfc06507 100644 --- a/src/app/store/effects/download.effects.ts +++ b/src/app/store/effects/download.effects.ts @@ -31,10 +31,15 @@ import { map } from 'rxjs/operators'; import { DownloadNodesAction, DOWNLOAD_NODES } from '../actions'; import { NodeInfo } from '../models'; import { ContentApiService } from '../../services/content-api.service'; +import { MinimalNodeEntity } from 'alfresco-js-api'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../states'; +import { appSelection } from '../selectors/app.selectors'; @Injectable() export class DownloadEffects { constructor( + private store: Store, private actions$: Actions, private contentApi: ContentApiService, private dialog: MatDialog @@ -42,15 +47,35 @@ export class DownloadEffects { @Effect({ dispatch: false }) downloadNode$ = this.actions$.pipe( - ofType(DOWNLOAD_NODES), - map(action => { - if (action.payload && action.payload.length > 0) { - this.downloadNodes(action.payload); - } - }) + ofType(DOWNLOAD_NODES), + map(action => { + if (action.payload && action.payload.length > 0) { + this.downloadNodes(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.downloadNodes(selection.nodes); + } + }); + } + }) ); - private downloadNodes(nodes: Array) { + private downloadNodes(toDownload: Array) { + const nodes = toDownload.map(node => { + const { id, nodeId, name, isFile, isFolder } = node.entry; + + return { + id: nodeId || id, + name, + isFile, + isFolder + }; + }); + if (!nodes || nodes.length === 0) { return; } From 85c0e42047417c30b10d967207da8b3c3098bdc9 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Mon, 9 Jul 2018 07:25:49 +0100 Subject: [PATCH 016/146] extensions: support multiselection for content actions --- src/app.config.json | 5 ++- .../extensions/content-action.extension.ts | 1 + src/app/extensions/extension.service.ts | 44 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/app.config.json b/src/app.config.json index a63abcfcc8..77853272d2 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -212,9 +212,10 @@ "title": "APP.ACTIONS.DOWNLOAD", "icon": "get_app", "target": { - "types": ["folder", "file"], + "types": ["file", "folder"], "permissions": [], - "action": "aca:actions/download" + "action": "aca:actions/download", + "multiple": true } }, { diff --git a/src/app/extensions/content-action.extension.ts b/src/app/extensions/content-action.extension.ts index 2209dd4acd..5126b09f22 100644 --- a/src/app/extensions/content-action.extension.ts +++ b/src/app/extensions/content-action.extension.ts @@ -33,5 +33,6 @@ export interface ContentActionExtension { types: Array; permissions: Array, action: string; + multiple?: boolean; }; } diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 3463a646a9..7aa9afce2c 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -237,12 +237,46 @@ export class ExtensionService { return true; } - if (selection && selection.folder && types.includes('folder')) { - return true; - } + if (selection && !selection.isEmpty) { - if (selection && selection.file && types.includes('file')) { - return true; + if (selection.nodes.length === 1) { + if (selection.folder && types.includes('folder')) { + return true; + } + if (selection.file && types.includes('file')) { + return true; + } + return false; + } else { + if (types.length === 1) { + if (types.includes('folder')) { + if (action.target.multiple) { + return selection.nodes.every(node => node.entry.isFolder); + } + return false; + } + if (types.includes('file')) { + if (action.target.multiple) { + return selection.nodes.every(node => node.entry.isFile); + } + return false; + } + } else { + return types.some(type => { + if (type === 'folder') { + return action.target.multiple + ? selection.nodes.some(node => node.entry.isFolder) + : selection.nodes.every(node => node.entry.isFolder); + } + if (type === 'file') { + return action.target.multiple + ? selection.nodes.some(node => node.entry.isFile) + : selection.nodes.every(node => node.entry.isFile); + } + return false; + }); + } + } } return false; From 7a95485a05cab709fc12c2c8f897dcc52eb76120 Mon Sep 17 00:00:00 2001 From: Suzana Dirla Date: Mon, 9 Jul 2018 17:20:54 +0300 Subject: [PATCH 017/146] [ACA-1113] Node Permissions - experimental (#501) * [ACA-1113] Integrate permissions * [ACA-1113] experimental flag for permissions * [ACA-1113] permissions shown only on write permission * [ACA-1113] remove console.logs --- src/app.config.json | 1 + src/app/app.module.ts | 9 +- .../directives/node-permissions.directive.ts | 80 ++++++++++++++++ .../favorites/favorites.component.html | 10 ++ src/app/components/files/files.component.html | 10 ++ src/app/components/page.component.ts | 2 + .../permissions-manager.component.html | 20 ++++ .../permissions-manager.component.theme.scss | 49 ++++++++++ .../permissions-manager.component.ts | 91 +++++++++++++++++++ .../components/preview/preview.component.html | 10 ++ .../recent-files/recent-files.component.html | 10 ++ .../search-results.component.html | 10 ++ .../shared-files/shared-files.component.html | 10 ++ .../node-permissions.dialog.html | 7 ++ .../node-permissions.dialog.ts | 42 +++++++++ src/app/ui/custom-theme.scss | 2 + src/assets/i18n/en.json | 9 ++ 17 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/app/common/directives/node-permissions.directive.ts create mode 100644 src/app/components/permission-manager/permissions-manager.component.html create mode 100644 src/app/components/permission-manager/permissions-manager.component.theme.scss create mode 100644 src/app/components/permission-manager/permissions-manager.component.ts create mode 100644 src/app/dialogs/node-permissions/node-permissions.dialog.html create mode 100644 src/app/dialogs/node-permissions/node-permissions.dialog.ts diff --git a/src/app.config.json b/src/app.config.json index 77853272d2..d0220dfbfb 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -12,6 +12,7 @@ "libraries": false, "comments": false, "cardview": false, + "permissions": false, "share": false, "extensions": false }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 81316b2f14..6bc9b1f848 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,9 @@ import { ExtensionsModule } from './extensions.module'; import { ExtensionService } from './extensions/extension.service'; import { CoreExtensionsModule } from './extensions/core.extensions'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; +import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; +import { NodePermissionsDirective } from './common/directives/node-permissions.directive'; +import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; @NgModule({ imports: [ @@ -127,7 +130,10 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro NodePermanentDeleteDirective, NodeUnshareDirective, NodeVersionsDirective, + NodePermissionsDirective, NodeVersionsDialogComponent, + NodePermissionsDialogComponent, + PermissionsManagerComponent, SearchResultsComponent, SettingsComponent, InfoDrawerComponent, @@ -156,7 +162,8 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro ExtensionService ], entryComponents: [ - NodeVersionsDialogComponent + NodeVersionsDialogComponent, + NodePermissionsDialogComponent ], bootstrap: [AppComponent] }) diff --git a/src/app/common/directives/node-permissions.directive.ts b/src/app/common/directives/node-permissions.directive.ts new file mode 100644 index 0000000000..6fe39cbe74 --- /dev/null +++ b/src/app/common/directives/node-permissions.directive.ts @@ -0,0 +1,80 @@ +/*! + * @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 { MinimalNodeEntity } from 'alfresco-js-api'; +import { MatDialog } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { SnackbarErrorAction } from '../../store/actions'; +import { NodePermissionsDialogComponent } from '../../dialogs/node-permissions/node-permissions.dialog'; + +@Directive({ + selector: '[acaNodePermissions]' +}) +export class NodePermissionsDirective { + // tslint:disable-next-line:no-input-rename + @Input('acaNodePermissions') node: MinimalNodeEntity; + + @HostListener('click') + onClick() { + this.showPermissions(); + } + + constructor( + private store: Store, + private dialog: MatDialog + ) {} + + showPermissions() { + if (this.node) { + let entry; + if (this.node.entry) { + entry = this.node.entry; + + } else { + entry = this.node; + } + + const entryId = entry.nodeId || (entry).guid || entry.id; + this.openPermissionsDialog(entryId); + } + } + + openPermissionsDialog(nodeId: string) { + // workaround Shared + if (nodeId) { + this.dialog.open(NodePermissionsDialogComponent, { + data: { nodeId }, + panelClass: 'aca-permissions-dialog-panel', + width: '730px' + }); + } else { + this.store.dispatch( + new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION') + ); + } + } +} diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 975c562aff..e64fd52a94 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -116,6 +116,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + + diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index a718f2bfef..d9ebe18266 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -121,6 +121,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + + diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 0bc69dca64..d551e08759 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -54,6 +54,7 @@ export abstract class PageComponent implements OnInit, OnDestroy { sharedPreviewUrl$: Observable; actions: Array = []; canUpdateFile = false; + canUpdateNode = false; canDelete = false; canEditFolder = false; canUpload = false; @@ -84,6 +85,7 @@ export abstract class PageComponent implements OnInit, OnDestroy { } this.actions = this.extensions.getSelectedContentActions(selection, this.node); this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file); + this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first); this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes); this.canEditFolder = selection.folder && this.content.canUpdateNode(selection.folder); this.canDeleteShared = !this.selection.isEmpty && this.content.canDeleteSharedNodes(selection.nodes); diff --git a/src/app/components/permission-manager/permissions-manager.component.html b/src/app/components/permission-manager/permissions-manager.component.html new file mode 100644 index 0000000000..555ca38c92 --- /dev/null +++ b/src/app/components/permission-manager/permissions-manager.component.html @@ -0,0 +1,20 @@ +
+ + +
+ +
+ + +
+ diff --git a/src/app/components/permission-manager/permissions-manager.component.theme.scss b/src/app/components/permission-manager/permissions-manager.component.theme.scss new file mode 100644 index 0000000000..71ce49ddf7 --- /dev/null +++ b/src/app/components/permission-manager/permissions-manager.component.theme.scss @@ -0,0 +1,49 @@ +@mixin aca-permissions-manager-theme($theme) { + $foreground: map-get($theme, foreground); + $accent: map-get($theme, accent); + + aca-permissions-dialog-panel { + height: 400px; + } + + .aca-node-permissions-dialog { + .mat-dialog-title { + font-size: 20px; + font-weight: 600; + font-style: normal; + font-stretch: normal; + line-height: 1.6; + margin: 0; + letter-spacing: -0.5px; + color: mat-color($foreground, text, 0.87); + } + + .mat-dialog-content { + flex: 1 1 auto; + position: relative; + overflow: auto; + + adf-permission-list { + display: flex; + } + } + + .mat-dialog-actions { + flex: 0 0 auto; + + padding: 8px 8px 24px 8px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + color: mat-color($foreground, text, 0.54); + + button { + text-transform: uppercase; + font-weight: normal; + } + } + } +} diff --git a/src/app/components/permission-manager/permissions-manager.component.ts b/src/app/components/permission-manager/permissions-manager.component.ts new file mode 100644 index 0000000000..08265659fe --- /dev/null +++ b/src/app/components/permission-manager/permissions-manager.component.ts @@ -0,0 +1,91 @@ +/*! + * @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 { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { NodePermissionDialogService, PermissionListComponent } from '@alfresco/adf-content-services'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states/app.state'; +import { SnackbarErrorAction } from '../../store/actions/snackbar.actions'; +import { NodePermissionsDialogComponent } from '../../dialogs/node-permissions/node-permissions.dialog'; +import { MatDialog } from '@angular/material'; +import { ContentApiService } from '../../services/content-api.service'; + +@Component({ + selector: 'aca-permissions-manager', + templateUrl: './permissions-manager.component.html' +}) +export class PermissionsManagerComponent implements OnInit { + @ViewChild('permissionList') + permissionList: PermissionListComponent; + + @Input() + nodeId: string; + + toggleStatus = false; + + constructor( + private store: Store, + private dialog: MatDialog, + private contentApi: ContentApiService, + private nodePermissionDialogService: NodePermissionDialogService + ) { + } + + ngOnInit() { + this.contentApi.getNodeInfo(this.nodeId, {include: ['permissions'] }).subscribe( (currentNode: MinimalNodeEntryEntity) => { + this.toggleStatus = currentNode.permissions.isInheritanceEnabled; + }); + } + + onError(errorMessage: string) { + this.store.dispatch(new SnackbarErrorAction(errorMessage)); + } + + onUpdate(event) { + this.permissionList.reload(); + } + + onUpdatedPermissions(node: MinimalNodeEntryEntity) { + this.toggleStatus = node.permissions.isInheritanceEnabled; + this.permissionList.reload(); + } + + openAddPermissionDialog(event: Event) { + this.nodePermissionDialogService.updateNodePermissionByDialog(this.nodeId) + .subscribe(() => { + this.dialog.open(NodePermissionsDialogComponent, { + data: { nodeId: this.nodeId }, + panelClass: 'aca-permissions-dialog-panel', + width: '800px' + } + ); + }, + (error) => { + this.store.dispatch(new SnackbarErrorAction(error)); + } + ); + } +} diff --git a/src/app/components/preview/preview.component.html b/src/app/components/preview/preview.component.html index b7c3600234..8dd91b2dce 100644 --- a/src/app/components/preview/preview.component.html +++ b/src/app/components/preview/preview.component.html @@ -75,6 +75,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + + diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index 9b64939449..0eec19a18f 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -109,6 +109,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + +
diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index ac2eeb0ce4..fcfb76bd58 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -72,6 +72,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + +
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 91c196ec95..65d67faf14 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -106,6 +106,16 @@ history {{ 'APP.ACTIONS.VERSIONS' | translate }} + + + +
diff --git a/src/app/dialogs/node-permissions/node-permissions.dialog.html b/src/app/dialogs/node-permissions/node-permissions.dialog.html new file mode 100644 index 0000000000..95ae67afe6 --- /dev/null +++ b/src/app/dialogs/node-permissions/node-permissions.dialog.html @@ -0,0 +1,7 @@ +
{{'PERMISSIONS.DIALOG.TITLE' | translate}}
+
+ +
+
+ +
diff --git a/src/app/dialogs/node-permissions/node-permissions.dialog.ts b/src/app/dialogs/node-permissions/node-permissions.dialog.ts new file mode 100644 index 0000000000..c026b29687 --- /dev/null +++ b/src/app/dialogs/node-permissions/node-permissions.dialog.ts @@ -0,0 +1,42 @@ +/*! + * @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 { Component, Inject, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material'; + +@Component({ + templateUrl: './node-permissions.dialog.html', + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-node-permissions-dialog' } +}) +export class NodePermissionsDialogComponent { + nodeId: string; + + constructor( + @Inject(MAT_DIALOG_DATA) data: any, + ) { + this.nodeId = data.nodeId; + } +} diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss index 00dd3cc92e..1bb506ee84 100644 --- a/src/app/ui/custom-theme.scss +++ b/src/app/ui/custom-theme.scss @@ -8,6 +8,7 @@ @import '../components/settings/settings.component.theme'; @import '../components/current-user/current-user.component.theme'; @import '../components/header/header.component.theme'; +@import '../components/permission-manager/permissions-manager.component.theme'; @import '../dialogs/node-versions/node-versions.dialog.theme'; @import './overrides/adf-toolbar.theme'; @@ -83,6 +84,7 @@ $custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent); @include aca-header-theme($theme); @include aca-search-input-theme($theme); @include aca-generic-error-theme($theme); + @include aca-permissions-manager-theme($theme); @include aca-node-versions-dialog-theme($theme); @include aca-settings-theme($theme); @include snackbar-theme($theme); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 10e678b8e1..b3c2fa9b69 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -120,6 +120,7 @@ "DELETE_PERMANENT": "Permanently delete", "MORE": "More actions", "UNDO": "Undo", + "PERMISSIONS": "Permissions", "RESTORE": "Restore", "FAVORITE": "Favorite", "UNSHARE": "Unshare", @@ -250,6 +251,14 @@ "MOVE_ITEMS": "Move {{ number }} items to...", "SEARCH": "Search" }, + "PERMISSIONS": { + "DIALOG": { + "TITLE": "Manage Permissons", + "CLOSE": "Close", + "INHERIT_PERMISSIONS_BUTTON": "Inherit Permission", + "INHERITED_PERMISSIONS_BUTTON": "Permission Inherited" + } + }, "VERSION": { "DIALOG": { "TITLE": "Manage Versions", From 382b459ac842167f5f69fdfa74a65801f8ad0dc9 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Mon, 9 Jul 2018 15:56:33 +0100 Subject: [PATCH 018/146] [ACA-1532] make Preview command available to extensions (#502) * view file action and effect * integrate view action with search input/row * deprecate old ViewNode action/effect * preview file command as extension * reorder commands --- src/app.config.json | 17 ++++ src/app/components/page.component.ts | 12 +-- .../search-input.component.spec.ts | 6 +- .../search-input/search-input.component.ts | 12 +-- .../search-results-row.component.ts | 17 ++-- src/app/store/actions/viewer.actions.ts | 10 +-- src/app/store/effects/viewer.effects.ts | 89 ++++++++++++++----- 7 files changed, 104 insertions(+), 59 deletions(-) diff --git a/src/app.config.json b/src/app.config.json index d0220dfbfb..e8f87d16fb 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -70,6 +70,11 @@ "type": "DOWNLOAD_NODES", "payload": null }, + { + "id": "aca:actions/preview", + "type": "VIEW_FILE", + "payload": null + }, { "id": "aca:actions/info", @@ -206,6 +211,18 @@ "action": "aca:actions/create-folder" } }, + { + "disabled": false, + "id": "aca:toolbar/preview", + "order": 15, + "title": "APP.ACTIONS.VIEW", + "icon": "open_in_browser", + "target": { + "types": ["file"], + "permissions": [], + "action": "aca:actions/preview" + } + }, { "disabled": false, "id": "aca:toolbar/download", diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index d551e08759..02f282ce66 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -30,7 +30,7 @@ import { Store } from '@ngrx/store'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; import { takeUntil } from 'rxjs/operators'; import { Subject, Subscription } from 'rxjs/Rx'; -import { ViewNodeAction, SetSelectedNodesAction, DownloadNodesAction } from '../store/actions'; +import { SetSelectedNodesAction, DownloadNodesAction, ViewFileAction } from '../store/actions'; import { appSelection, sharedUrl, currentFolder } from '../store/selectors/app.selectors'; import { AppStore } from '../store/states/app.state'; import { SelectionState } from '../store/states/selection.state'; @@ -109,16 +109,8 @@ export abstract class PageComponent implements OnInit, OnDestroy { showPreview(node: MinimalNodeEntity) { if (node && node.entry) { - const { id, nodeId, name, isFile, isFolder } = node.entry; const parentId = this.node ? this.node.id : null; - - this.store.dispatch(new ViewNodeAction({ - parentId, - id: nodeId || id, - name, - isFile, - isFolder - })); + this.store.dispatch(new ViewFileAction(node, parentId)); } } diff --git a/src/app/components/search/search-input/search-input.component.spec.ts b/src/app/components/search/search-input/search-input.component.spec.ts index a1e7b421e4..59d5a0614b 100644 --- a/src/app/components/search/search-input/search-input.component.spec.ts +++ b/src/app/components/search/search-input/search-input.component.spec.ts @@ -29,7 +29,7 @@ import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core import { SearchInputComponent } from './search-input.component'; import { AppTestingModule } from '../../../testing/app-testing.module'; import { Actions, ofType } from '@ngrx/effects'; -import { ViewNodeAction, VIEW_NODE, NAVIGATE_FOLDER, NavigateToFolder } from '../../../store/actions'; +import { NAVIGATE_FOLDER, NavigateToFolder, VIEW_FILE, ViewFileAction } from '../../../store/actions'; import { map } from 'rxjs/operators'; describe('SearchInputComponent', () => { @@ -59,9 +59,9 @@ describe('SearchInputComponent', () => { describe('onItemClicked()', () => { it('opens preview if node is file', fakeAsync(done => { actions$.pipe( - ofType(VIEW_NODE), + ofType(VIEW_FILE), map(action => { - expect(action.payload.id).toBe('node-id'); + expect(action.payload.entry.id).toBe('node-id'); done(); }) ); diff --git a/src/app/components/search/search-input/search-input.component.ts b/src/app/components/search/search-input/search-input.component.ts index c05e5ccf44..3a74cdb056 100644 --- a/src/app/components/search/search-input/search-input.component.ts +++ b/src/app/components/search/search-input/search-input.component.ts @@ -32,7 +32,7 @@ import { MinimalNodeEntity } from 'alfresco-js-api'; import { SearchInputControlComponent } from '../search-input-control/search-input-control.component'; import { Store } from '@ngrx/store'; import { AppStore } from '../../../store/states/app.state'; -import { SearchByTermAction, ViewNodeAction, NavigateToFolder } from '../../../store/actions'; +import { SearchByTermAction, NavigateToFolder, ViewFileAction } from '../../../store/actions'; @Component({ selector: 'aca-search-input', @@ -90,15 +90,9 @@ export class SearchInputComponent implements OnInit { onItemClicked(node: MinimalNodeEntity) { if (node && node.entry) { - const { id, nodeId, name, isFile, isFolder, parentId } = node.entry; + const { isFile, isFolder } = node.entry; if (isFile) { - this.store.dispatch(new ViewNodeAction({ - parentId, - id: nodeId || id, - name, - isFile, - isFolder - })); + this.store.dispatch(new ViewFileAction(node)); } else if (isFolder) { this.store.dispatch(new NavigateToFolder(node)); } diff --git a/src/app/components/search/search-results-row/search-results-row.component.ts b/src/app/components/search/search-results-row/search-results-row.component.ts index 154229d702..163b47a92d 100644 --- a/src/app/components/search/search-results-row/search-results-row.component.ts +++ b/src/app/components/search/search-results-row/search-results-row.component.ts @@ -24,8 +24,8 @@ */ import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; -import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { ViewNodeAction } from '../../../store/actions/viewer.actions'; +import { MinimalNodeEntity } from 'alfresco-js-api'; +import { ViewFileAction } from '../../../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../../../store/states/app.state'; @@ -38,14 +38,14 @@ import { AppStore } from '../../../store/states/app.state'; host: { class: 'aca-search-results-row' } }) export class SearchResultsRowComponent implements OnInit { - private node: MinimalNodeEntryEntity; + private node: MinimalNodeEntity; @Input() context: any; constructor(private store: Store) {} ngOnInit() { - this.node = this.context.row.node.entry; + this.node = this.context.row.node; } get name() { @@ -89,13 +89,8 @@ export class SearchResultsRowComponent implements OnInit { } showPreview() { - const { id, name } = this.node; - this.store.dispatch( - new ViewNodeAction({ - id, - name - }) + new ViewFileAction(this.node) ); } @@ -106,6 +101,6 @@ export class SearchResultsRowComponent implements OnInit { .replace('[', '.') .replace(']', '') .split('.') - .reduce((acc, part) => (acc ? acc[part] : null), this.node); + .reduce((acc, part) => (acc ? acc[part] : null), this.node.entry); } } diff --git a/src/app/store/actions/viewer.actions.ts b/src/app/store/actions/viewer.actions.ts index 048a408ebd..7d2ea5bea1 100644 --- a/src/app/store/actions/viewer.actions.ts +++ b/src/app/store/actions/viewer.actions.ts @@ -24,11 +24,11 @@ */ import { Action } from '@ngrx/store'; -import { NodeInfo } from '../models'; +import { MinimalNodeEntity } from 'alfresco-js-api'; -export const VIEW_NODE = 'VIEW_NODE'; +export const VIEW_FILE = 'VIEW_FILE'; -export class ViewNodeAction implements Action { - readonly type = VIEW_NODE; - constructor(public payload: NodeInfo) {} +export class ViewFileAction implements Action { + readonly type = VIEW_FILE; + constructor(public payload: MinimalNodeEntity, public parentId?: string) {} } diff --git a/src/app/store/effects/viewer.effects.ts b/src/app/store/effects/viewer.effects.ts index 42a97bb2e9..ccac612ef9 100644 --- a/src/app/store/effects/viewer.effects.ts +++ b/src/app/store/effects/viewer.effects.ts @@ -26,37 +26,84 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { Injectable } from '@angular/core'; import { map } from 'rxjs/operators'; -import { ViewNodeAction, VIEW_NODE } from '../actions/viewer.actions'; +import { VIEW_FILE, ViewFileAction } from '../actions'; import { Router } from '@angular/router'; +import { Store, createSelector } from '@ngrx/store'; +import { AppStore } from '../states'; +import { appSelection, currentFolder } from '../selectors/app.selectors'; + +export const fileToPreview = createSelector( + appSelection, + currentFolder, + (selection, folder) => { + return { + selection, + folder + }; + } +); @Injectable() export class ViewerEffects { - constructor(private actions$: Actions, private router: Router) {} + constructor( + private store: Store, + private actions$: Actions, + private router: Router + ) {} @Effect({ dispatch: false }) - viewNode$ = this.actions$.pipe( - ofType(VIEW_NODE), + viewFile$ = this.actions$.pipe( + ofType(VIEW_FILE), map(action => { - const node = action.payload; - if (!node) { - return; - } + if (action.payload && action.payload.entry) { + const { id, nodeId, isFile } = action.payload.entry; - let previewLocation = this.router.url; - if (previewLocation.lastIndexOf('/') > 0) { - previewLocation = previewLocation.substr( - 0, - this.router.url.indexOf('/', 1) - ); - } - previewLocation = previewLocation.replace(/\//g, ''); + if (isFile || nodeId) { + this.displayPreview(nodeId || id, action.parentId); + } + } else { + this.store + .select(fileToPreview) + .take(1) + .subscribe(result => { + if (result.selection && result.selection.file) { + const { + id, + nodeId, + isFile + } = result.selection.file.entry; - const path = [previewLocation]; - if (node.parentId) { - path.push(node.parentId); + if (isFile || nodeId) { + const parentId = result.folder + ? result.folder.id + : null; + this.displayPreview(nodeId || id, parentId); + } + } + }); } - path.push('preview', node.id); - this.router.navigateByUrl(path.join('/')); }) ); + + private displayPreview(nodeId: string, parentId: string) { + if (!nodeId) { + return; + } + + let previewLocation = this.router.url; + if (previewLocation.lastIndexOf('/') > 0) { + previewLocation = previewLocation.substr( + 0, + this.router.url.indexOf('/', 1) + ); + } + previewLocation = previewLocation.replace(/\//g, ''); + + const path = [previewLocation]; + if (parentId) { + path.push(parentId); + } + path.push('preview', nodeId); + this.router.navigateByUrl(path.join('/')); + } } From 64b0790a0d7cc19b643c9ecfca4161b14e9caccf Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Mon, 9 Jul 2018 22:20:13 +0300 Subject: [PATCH 019/146] [ACA-1543] Quick Share - Non-logged user can't see the content of a shared file (#503) * resolve guard * lint * fix errors --- .../services/experimental-guard.service.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/app/common/services/experimental-guard.service.ts b/src/app/common/services/experimental-guard.service.ts index 5750f5fa46..237541cb75 100644 --- a/src/app/common/services/experimental-guard.service.ts +++ b/src/app/common/services/experimental-guard.service.ts @@ -1,20 +1,35 @@ -import { CanActivate, ActivatedRouteSnapshot } from '@angular/router'; +import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; import { Injectable } from '@angular/core'; -import { AppConfigService } from '@alfresco/adf-core'; +import { AppConfigService, StorageService } from '@alfresco/adf-core'; import { environment } from '../../../environments/environment'; @Injectable() export class ExperimentalGuard implements CanActivate { - constructor(private config: AppConfigService) {} + constructor( + private config: AppConfigService, + private storage: StorageService, + private router: Router + ) {} canActivate(route: ActivatedRouteSnapshot) { const key = `experimental.${route.data.ifExperimentalKey}`; const value = this.config.get(key); + const override = this.storage.getItem(key); + + if (override === 'true') { + return true; + } if (!environment.production) { - return value === true || value === 'true'; + if (value === true || value === 'true') { + return true; + } + + this.router.navigate(['/']); } + this.router.navigate(['/']); + return false; } } From ad9ce9e88f441ba7cf9b8dcb621083d48bfadd0b Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Tue, 10 Jul 2018 20:39:07 +0100 Subject: [PATCH 020/146] [ACA-1544] extensions: toolbar separators and menus (#504) * toolbar separators * remove the need for "target" for separators * simplify code * menu stub, reducing separators * toolbar action component * render menu items * menu items --- src/app.config.json | 69 +++++++---- .../favorites/favorites.component.html | 12 +- src/app/components/files/files.component.html | 12 +- src/app/components/page.component.ts | 3 +- .../recent-files/recent-files.component.html | 12 +- .../search-results.component.html | 12 +- .../shared-files/shared-files.component.html | 12 +- .../trashcan/trashcan.component.html | 12 +- .../toolbar-action.component.html | 30 +++++ .../toolbar-action.component.ts | 81 +++++++++++++ .../extensions/content-action.extension.ts | 9 ++ src/app/extensions/core.extensions.ts | 12 +- src/app/extensions/extension.service.ts | 109 ++++++++++-------- src/app/extensions/utils.ts | 87 ++++++++++++++ 14 files changed, 343 insertions(+), 129 deletions(-) create mode 100644 src/app/extensions/components/toolbar-action/toolbar-action.component.html create mode 100644 src/app/extensions/components/toolbar-action/toolbar-action.component.ts create mode 100644 src/app/extensions/utils.ts diff --git a/src/app.config.json b/src/app.config.json index e8f87d16fb..39041e5ec0 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -200,8 +200,13 @@ "content": { "actions": [ { - "disabled": false, + "id": "aca:toolbar/separator-1", + "order": 5, + "type": "separator" + }, + { "id": "aca:toolbar/create-folder", + "type": "button", "order": 10, "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", "icon": "create_new_folder", @@ -212,8 +217,8 @@ } }, { - "disabled": false, "id": "aca:toolbar/preview", + "type": "button", "order": 15, "title": "APP.ACTIONS.VIEW", "icon": "open_in_browser", @@ -224,8 +229,8 @@ } }, { - "disabled": false, "id": "aca:toolbar/download", + "type": "button", "order": 20, "title": "APP.ACTIONS.DOWNLOAD", "icon": "get_app", @@ -237,8 +242,8 @@ } }, { - "disabled": false, "id": "aca:toolbar/edit-folder", + "type": "button", "order": 30, "title": "APP.ACTIONS.EDIT", "icon": "create", @@ -249,30 +254,44 @@ } }, - { - "disabled": false, - "id": "aca:action3", - "order": 101, - "title": "Settings", - "icon": "settings_applications", - "target": { - "types": [], - "permissions": [], - "action": "aca:actions/settings" - } + "id": "aca:toolbar/separator-2", + "order": 200, + "type": "separator" }, { - "disabled": false, - "id": "aca:action4", - "order": 101, - "title": "Error", - "icon": "report_problem", - "target": { - "types": ["file"], - "permissions": ["update", "delete"], - "action": "aca:actions/error" - } + "id": "aca:toolbar/menu-1", + "type": "menu", + "icon": "storage", + "order": 300, + "children": [ + { + "id": "aca:action3", + "type": "button", + "title": "Settings", + "icon": "settings_applications", + "target": { + "types": [], + "permissions": [], + "action": "aca:actions/settings" + } + }, + { + "id": "aca:action4", + "type": "button", + "title": "Error", + "icon": "report_problem", + "target": { + "types": ["file"], + "permissions": ["update", "delete"], + "action": "aca:actions/error" + } + } + ] + }, + { + "id": "aca:toolbar/separator-3", + "type": "separator" } ] } diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index e64fd52a94..4105319d7b 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -13,15 +13,9 @@ - - - + + + diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index d9ebe18266..00a54cfa4a 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -16,15 +16,9 @@ - - - + + + diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 02f282ce66..e84c547a0f 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -83,7 +83,8 @@ export abstract class PageComponent implements OnInit, OnDestroy { if (selection.isEmpty) { this.infoDrawerOpened = false; } - this.actions = this.extensions.getSelectedContentActions(selection, this.node); + const selectedNodes = selection ? selection.nodes : null; + this.actions = this.extensions.getAllowedContentActions(selectedNodes, this.node); this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file); this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first); this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes); diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index 0eec19a18f..1f6c334de4 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -13,15 +13,9 @@ - - - + + + diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index fcfb76bd58..68976ed8ee 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -4,15 +4,9 @@ - - - + + + diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 65d67faf14..786766de15 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -13,15 +13,9 @@ - - - + + + diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html index 6c9570de8a..0515ed64d0 100644 --- a/src/app/components/trashcan/trashcan.component.html +++ b/src/app/components/trashcan/trashcan.component.html @@ -13,15 +13,9 @@ - - - + + + diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html new file mode 100644 index 0000000000..c4e03c64d3 --- /dev/null +++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts b/src/app/extensions/components/toolbar-action/toolbar-action.component.ts new file mode 100644 index 0000000000..5225fa07f7 --- /dev/null +++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.ts @@ -0,0 +1,81 @@ +/*! + * @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 { + Component, + ViewEncapsulation, + ChangeDetectionStrategy, + Input, + OnInit, + OnDestroy +} from '@angular/core'; +import { ContentActionExtension } from '../../content-action.extension'; +import { AppStore, SelectionState } from '../../../store/states'; +import { Store } from '@ngrx/store'; +import { ExtensionService } from '../../extension.service'; +import { appSelection } from '../../../store/selectors/app.selectors'; +import { Subject } from 'rxjs/Rx'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'aca-toolbar-action', + templateUrl: './toolbar-action.component.html', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'aca-toolbar-action' } +}) +export class ToolbarActionComponent implements OnInit, OnDestroy { + @Input() entry: ContentActionExtension; + + selection: SelectionState; + onDestroy$: Subject = new Subject(); + + constructor( + protected store: Store, + protected extensions: ExtensionService + ) {} + + ngOnInit() { + this.store + .select(appSelection) + .pipe(takeUntil(this.onDestroy$)) + .subscribe(selection => { + this.selection = selection; + }); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + runAction(actionId: string) { + const context = { + selection: this.selection + }; + + this.extensions.runActionById(actionId, context); + } +} diff --git a/src/app/extensions/content-action.extension.ts b/src/app/extensions/content-action.extension.ts index 5126b09f22..0f4478c2dc 100644 --- a/src/app/extensions/content-action.extension.ts +++ b/src/app/extensions/content-action.extension.ts @@ -23,12 +23,21 @@ * along with Alfresco. If not, see . */ +export enum ContentActionType { + default = 'button', + button = 'button', + separator = 'separator', + menu = 'menu' +} + export interface ContentActionExtension { id: string; + type: ContentActionType; order?: number; title: string; icon?: string; disabled?: boolean; + children?: Array; target: { types: Array; permissions: Array, diff --git a/src/app/extensions/core.extensions.ts b/src/app/extensions/core.extensions.ts index f32e91d940..e58bfc6cbb 100644 --- a/src/app/extensions/core.extensions.ts +++ b/src/app/extensions/core.extensions.ts @@ -24,14 +24,20 @@ */ import { NgModule } from '@angular/core'; -import { AuthGuardEcm } from '@alfresco/adf-core'; +import { AuthGuardEcm, CoreModule } from '@alfresco/adf-core'; import { ExtensionService } from './extension.service'; import { AboutComponent } from '../components/about/about.component'; import { LayoutComponent } from '../components/layout/layout.component'; +import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component'; +import { CommonModule } from '@angular/common'; @NgModule({ - imports: [], - declarations: [], + imports: [ + CommonModule, + CoreModule.forChild() + ], + declarations: [ToolbarActionComponent], + exports: [ToolbarActionComponent], entryComponents: [AboutComponent] }) export class CoreExtensionsModule { diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 7aa9afce2c..d17ad4c755 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -27,13 +27,14 @@ import { Injectable, Type } from '@angular/core'; import { RouteExtension } from './route.extension'; import { ActionExtension } from './action.extension'; import { AppConfigService } from '@alfresco/adf-core'; -import { ContentActionExtension } from './content-action.extension'; +import { ContentActionExtension, ContentActionType } from './content-action.extension'; import { OpenWithExtension } from './open-with.extension'; -import { AppStore, SelectionState } from '../store/states'; +import { AppStore } from '../store/states'; import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; -import { Node } from 'alfresco-js-api'; +import { Node, MinimalNodeEntity } from 'alfresco-js-api'; +import { reduceSeparators, sortByOrder, filterEnabled, copyAction, reduceEmptyMenus } from './utils'; @Injectable() export class ExtensionService { @@ -70,7 +71,7 @@ export class ExtensionService { 'extensions.core.features.content.actions', [] ) - .sort(this.sortByOrder); + .sort(sortByOrder); this.openWithActions = this.config .get>( @@ -78,14 +79,14 @@ export class ExtensionService { [] ) .filter(entry => !entry.disabled) - .sort(this.sortByOrder); + .sort(sortByOrder); this.createActions = this.config .get>( 'extensions.core.features.create', [] ) - .sort(this.sortByOrder); + .sort(sortByOrder); } getRouteById(id: string): RouteExtension { @@ -178,7 +179,7 @@ export class ExtensionService { // evaluates create actions for the folder node getFolderCreateActions(folder: Node): Array { - return this.createActions.filter(this.filterOutDisabled).map(action => { + return this.createActions.filter(filterEnabled).map(action => { if ( action.target && action.target.permissions && @@ -200,64 +201,72 @@ export class ExtensionService { } // evaluates content actions for the selection and parent folder node - getSelectedContentActions( - selection: SelectionState, + getAllowedContentActions( + nodes: MinimalNodeEntity[], parentNode: Node ): Array { return this.contentActions - .filter(this.filterOutDisabled) - .filter(action => action.target) - .filter(action => this.filterByTarget(selection, action)) - .filter(action => - this.filterByPermission(selection, action, parentNode) - ); - } - - private sortByOrder( - a: { order?: number | undefined }, - b: { order?: number | undefined } - ) { - const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order; - const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; - return left - right; - } - - private filterOutDisabled(entry: { disabled?: boolean }): boolean { - return !entry.disabled; + .filter(filterEnabled) + .filter(action => this.filterByTarget(nodes, action)) + .filter(action => this.filterByPermission(nodes, action, parentNode)) + .reduce(reduceSeparators, []) + .map(action => { + if (action.type === ContentActionType.menu) { + const copy = copyAction(action); + if (copy.children && copy.children.length > 0) { + copy.children = copy.children + .filter(childAction => this.filterByTarget(nodes, childAction)) + .filter(childAction => this.filterByPermission(nodes, childAction, parentNode)) + .reduce(reduceSeparators, []); + } + return copy; + } + return action; + }) + .reduce(reduceEmptyMenus, []); } - // todo: support multiple selected nodes private filterByTarget( - selection: SelectionState, + nodes: MinimalNodeEntity[], action: ContentActionExtension ): boolean { + + if (!action) { + return false; + } + + if (!action.target) { + return action.type === ContentActionType.separator + || action.type === ContentActionType.menu; + } + const types = action.target.types; if (!types || types.length === 0) { return true; } - if (selection && !selection.isEmpty) { + if (nodes && nodes.length > 0) { - if (selection.nodes.length === 1) { - if (selection.folder && types.includes('folder')) { - return true; + if (nodes.length === 1) { + if (types.includes('folder')) { + return nodes.every(node => node.entry.isFolder); } - if (selection.file && types.includes('file')) { - return true; + if (types.includes('file')) { + return nodes.every(node => node.entry.isFile); } return false; } else { if (types.length === 1) { if (types.includes('folder')) { if (action.target.multiple) { - return selection.nodes.every(node => node.entry.isFolder); + return nodes.every(node => node.entry.isFolder); } return false; } if (types.includes('file')) { if (action.target.multiple) { - return selection.nodes.every(node => node.entry.isFile); + return nodes.every(node => node.entry.isFile); } return false; } @@ -265,13 +274,13 @@ export class ExtensionService { return types.some(type => { if (type === 'folder') { return action.target.multiple - ? selection.nodes.some(node => node.entry.isFolder) - : selection.nodes.every(node => node.entry.isFolder); + ? nodes.some(node => node.entry.isFolder) + : nodes.every(node => node.entry.isFolder); } if (type === 'file') { return action.target.multiple - ? selection.nodes.some(node => node.entry.isFile) - : selection.nodes.every(node => node.entry.isFile); + ? nodes.some(node => node.entry.isFile) + : nodes.every(node => node.entry.isFile); } return false; }); @@ -284,10 +293,19 @@ export class ExtensionService { // todo: support multiple selected nodes private filterByPermission( - selection: SelectionState, + nodes: MinimalNodeEntity[], action: ContentActionExtension, parentNode: Node ): boolean { + if (!action) { + return false; + } + + if (!action.target) { + return action.type === ContentActionType.separator + || action.type === ContentActionType.menu; + } + const permissions = action.target.permissions; if (!permissions || permissions.length === 0) { @@ -298,15 +316,14 @@ export class ExtensionService { if (permission.startsWith('parent.')) { if (parentNode) { const parentQuery = permission.split('.')[1]; - // console.log(parentNode.allowableOperations, parentQuery); return this.nodeHasPermissions(parentNode, [parentQuery]); } return false; } - if (selection && selection.first) { + if (nodes && nodes.length > 0) { return this.nodeHasPermissions( - selection.first.entry, + nodes[0].entry, permissions ); } diff --git a/src/app/extensions/utils.ts b/src/app/extensions/utils.ts new file mode 100644 index 0000000000..5bc44cb710 --- /dev/null +++ b/src/app/extensions/utils.ts @@ -0,0 +1,87 @@ +/*! + * @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 { + ContentActionExtension, + ContentActionType +} from './content-action.extension'; + +export function reduceSeparators( + acc: ContentActionExtension[], + el: ContentActionExtension, + i: number, + arr: ContentActionExtension[] +): ContentActionExtension[] { + // remove duplicate separators + if (i > 0) { + const prev = arr[i - 1]; + if ( + prev.type === ContentActionType.separator && + el.type === ContentActionType.separator + ) { + return acc; + } + } + // remove trailing separator + if (i === arr.length - 1) { + if (el.type === ContentActionType.separator) { + return acc; + } + } + return acc.concat(el); +} + + +export function reduceEmptyMenus( + acc: ContentActionExtension[], + el: ContentActionExtension +): ContentActionExtension[] { + if (el.type === ContentActionType.menu) { + if ((el.children || []).length === 0) { + return acc; + } + } + return acc.concat(el); +} + +export function sortByOrder( + a: { order?: number | undefined }, + b: { order?: number | undefined } +) { + const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order; + const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; + return left - right; +} + +export function filterEnabled(entry: { disabled?: boolean }): boolean { + return !entry.disabled; +} + +export function copyAction(action: ContentActionExtension): ContentActionExtension { + return { + ...action, + children: (action.children || []).map(copyAction) + }; +} From 8cd6d29b23693f13677c98c9ca55553260f39a3a Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Tue, 10 Jul 2018 20:52:33 +0100 Subject: [PATCH 021/146] migrate to HttpClient as per Angualar 5/6 prep --- src/app/components/about/about.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/components/about/about.component.ts b/src/app/components/about/about.component.ts index 3b5cb988f8..77b4ced918 100644 --- a/src/app/components/about/about.component.ts +++ b/src/app/components/about/about.component.ts @@ -24,7 +24,7 @@ */ import { Component, OnInit } from '@angular/core'; -import { Http } from '@angular/http'; +import { HttpClient } from '@angular/common/http'; import { ObjectDataTableAdapter } from '@alfresco/adf-core'; import { ContentApiService } from '../../services/content-api.service'; import { RepositoryInfo } from 'alfresco-js-api'; @@ -44,7 +44,7 @@ export class AboutComponent implements OnInit { constructor( private contentApi: ContentApiService, - private http: Http + private http: HttpClient ) {} ngOnInit() { @@ -85,8 +85,7 @@ export class AboutComponent implements OnInit { }); this.http.get('/versions.json') - .map(response => response.json()) - .subscribe(response => { + .subscribe((response: any) => { const regexp = new RegExp('^(@alfresco|alfresco-)'); const alfrescoPackagesTableRepresentation = Object.keys(response.dependencies) From 98a104fc49622436466b4c80a91feba62be56091 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Thu, 12 Jul 2018 07:06:02 +0100 Subject: [PATCH 022/146] merge utils back to extension service --- src/app/extensions/extension.service.ts | 204 +++++++++++++++--------- src/app/extensions/utils.ts | 87 ---------- 2 files changed, 131 insertions(+), 160 deletions(-) delete mode 100644 src/app/extensions/utils.ts diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index d17ad4c755..1cdd32bc25 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -34,7 +34,6 @@ import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; import { Node, MinimalNodeEntity } from 'alfresco-js-api'; -import { reduceSeparators, sortByOrder, filterEnabled, copyAction, reduceEmptyMenus } from './utils'; @Injectable() export class ExtensionService { @@ -71,7 +70,7 @@ export class ExtensionService { 'extensions.core.features.content.actions', [] ) - .sort(sortByOrder); + .sort(this.sortByOrder); this.openWithActions = this.config .get>( @@ -79,14 +78,14 @@ export class ExtensionService { [] ) .filter(entry => !entry.disabled) - .sort(sortByOrder); + .sort(this.sortByOrder); this.createActions = this.config .get>( 'extensions.core.features.create', [] ) - .sort(sortByOrder); + .sort(this.sortByOrder); } getRouteById(id: string): RouteExtension { @@ -179,7 +178,7 @@ export class ExtensionService { // evaluates create actions for the folder node getFolderCreateActions(folder: Node): Array { - return this.createActions.filter(filterEnabled).map(action => { + return this.createActions.filter(this.filterEnabled).map(action => { if ( action.target && action.target.permissions && @@ -206,31 +205,32 @@ export class ExtensionService { parentNode: Node ): Array { return this.contentActions - .filter(filterEnabled) + .filter(this.filterEnabled) .filter(action => this.filterByTarget(nodes, action)) .filter(action => this.filterByPermission(nodes, action, parentNode)) - .reduce(reduceSeparators, []) + .reduce(this.reduceSeparators, []) .map(action => { if (action.type === ContentActionType.menu) { - const copy = copyAction(action); + const copy = this.copyAction(action); if (copy.children && copy.children.length > 0) { copy.children = copy.children .filter(childAction => this.filterByTarget(nodes, childAction)) .filter(childAction => this.filterByPermission(nodes, childAction, parentNode)) - .reduce(reduceSeparators, []); + .reduce(this.reduceSeparators, []); } return copy; } return action; }) - .reduce(reduceEmptyMenus, []); + .reduce(this.reduceEmptyMenus, []); } - private filterByTarget( + // todo: support multiple selected nodes + private filterByPermission( nodes: MinimalNodeEntity[], - action: ContentActionExtension + action: ContentActionExtension, + parentNode: Node ): boolean { - if (!action) { return false; } @@ -240,6 +240,124 @@ export class ExtensionService { || action.type === ContentActionType.menu; } + const permissions = action.target.permissions; + + if (!permissions || permissions.length === 0) { + return true; + } + + return permissions.some(permission => { + if (permission.startsWith('parent.')) { + if (parentNode) { + const parentQuery = permission.split('.')[1]; + return this.nodeHasPermissions(parentNode, [parentQuery]); + } + return false; + } + + if (nodes && nodes.length > 0) { + return this.nodeHasPermissions( + nodes[0].entry, + permissions + ); + } + + return true; + }); + + return true; + } + + private nodeHasPermissions( + node: Node, + permissions: string[] = [] + ): boolean { + if ( + node && + node.allowableOperations && + node.allowableOperations.length > 0 + ) { + return permissions.some(permission => + node.allowableOperations.includes(permission) + ); + } + return false; + } + + private reduceSeparators( + acc: ContentActionExtension[], + el: ContentActionExtension, + i: number, + arr: ContentActionExtension[] + ): ContentActionExtension[] { + // remove duplicate separators + if (i > 0) { + const prev = arr[i - 1]; + if ( + prev.type === ContentActionType.separator && + el.type === ContentActionType.separator + ) { + return acc; + } + } + // remove trailing separator + if (i === arr.length - 1) { + if (el.type === ContentActionType.separator) { + return acc; + } + } + return acc.concat(el); + } + + private reduceEmptyMenus( + acc: ContentActionExtension[], + el: ContentActionExtension + ): ContentActionExtension[] { + if (el.type === ContentActionType.menu) { + if ((el.children || []).length === 0) { + return acc; + } + } + return acc.concat(el); + } + + private sortByOrder( + a: { order?: number | undefined }, + b: { order?: number | undefined } + ) { + const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order; + const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; + return left - right; + } + + private filterEnabled(entry: { disabled?: boolean }): boolean { + return !entry.disabled; + } + + private copyAction( + action: ContentActionExtension + ): ContentActionExtension { + return { + ...action, + children: (action.children || []).map(child => this.copyAction(child)) + }; + } + + private filterByTarget( + nodes: MinimalNodeEntity[], + action: ContentActionExtension + ): boolean { + if (!action) { + return false; + } + + if (!action.target) { + return ( + action.type === ContentActionType.separator || + action.type === ContentActionType.menu + ); + } + const types = action.target.types; if (!types || types.length === 0) { @@ -247,7 +365,6 @@ export class ExtensionService { } if (nodes && nodes.length > 0) { - if (nodes.length === 1) { if (types.includes('folder')) { return nodes.every(node => node.entry.isFolder); @@ -290,63 +407,4 @@ export class ExtensionService { return false; } - - // todo: support multiple selected nodes - private filterByPermission( - nodes: MinimalNodeEntity[], - action: ContentActionExtension, - parentNode: Node - ): boolean { - if (!action) { - return false; - } - - if (!action.target) { - return action.type === ContentActionType.separator - || action.type === ContentActionType.menu; - } - - const permissions = action.target.permissions; - - if (!permissions || permissions.length === 0) { - return true; - } - - return permissions.some(permission => { - if (permission.startsWith('parent.')) { - if (parentNode) { - const parentQuery = permission.split('.')[1]; - return this.nodeHasPermissions(parentNode, [parentQuery]); - } - return false; - } - - if (nodes && nodes.length > 0) { - return this.nodeHasPermissions( - nodes[0].entry, - permissions - ); - } - - return true; - }); - - return true; - } - - private nodeHasPermissions( - node: Node, - permissions: string[] = [] - ): boolean { - if ( - node && - node.allowableOperations && - node.allowableOperations.length > 0 - ) { - return permissions.some(permission => - node.allowableOperations.includes(permission) - ); - } - return false; - } } diff --git a/src/app/extensions/utils.ts b/src/app/extensions/utils.ts deleted file mode 100644 index 5bc44cb710..0000000000 --- a/src/app/extensions/utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -/*! - * @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 { - ContentActionExtension, - ContentActionType -} from './content-action.extension'; - -export function reduceSeparators( - acc: ContentActionExtension[], - el: ContentActionExtension, - i: number, - arr: ContentActionExtension[] -): ContentActionExtension[] { - // remove duplicate separators - if (i > 0) { - const prev = arr[i - 1]; - if ( - prev.type === ContentActionType.separator && - el.type === ContentActionType.separator - ) { - return acc; - } - } - // remove trailing separator - if (i === arr.length - 1) { - if (el.type === ContentActionType.separator) { - return acc; - } - } - return acc.concat(el); -} - - -export function reduceEmptyMenus( - acc: ContentActionExtension[], - el: ContentActionExtension -): ContentActionExtension[] { - if (el.type === ContentActionType.menu) { - if ((el.children || []).length === 0) { - return acc; - } - } - return acc.concat(el); -} - -export function sortByOrder( - a: { order?: number | undefined }, - b: { order?: number | undefined } -) { - const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order; - const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; - return left - right; -} - -export function filterEnabled(entry: { disabled?: boolean }): boolean { - return !entry.disabled; -} - -export function copyAction(action: ContentActionExtension): ContentActionExtension { - return { - ...action, - children: (action.children || []).map(copyAction) - }; -} From b1554b036ea9e1195853dd2d61b05b5f93a486c8 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Thu, 12 Jul 2018 07:40:16 +0100 Subject: [PATCH 023/146] fix target evaluation for extensions --- src/app/extensions/extension.service.ts | 45 ++++++------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 1cdd32bc25..adcf40f2c7 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -365,44 +365,19 @@ export class ExtensionService { } if (nodes && nodes.length > 0) { - if (nodes.length === 1) { - if (types.includes('folder')) { - return nodes.every(node => node.entry.isFolder); + return types.some(type => { + if (type === 'folder') { + return action.target.multiple + ? nodes.some(node => node.entry.isFolder) + : nodes.every(node => node.entry.isFolder); } - if (types.includes('file')) { - return nodes.every(node => node.entry.isFile); + if (type === 'file') { + return action.target.multiple + ? nodes.some(node => node.entry.isFile) + : nodes.every(node => node.entry.isFile); } return false; - } else { - if (types.length === 1) { - if (types.includes('folder')) { - if (action.target.multiple) { - return nodes.every(node => node.entry.isFolder); - } - return false; - } - if (types.includes('file')) { - if (action.target.multiple) { - return nodes.every(node => node.entry.isFile); - } - return false; - } - } else { - return types.some(type => { - if (type === 'folder') { - return action.target.multiple - ? nodes.some(node => node.entry.isFolder) - : nodes.every(node => node.entry.isFolder); - } - if (type === 'file') { - return action.target.multiple - ? nodes.some(node => node.entry.isFile) - : nodes.every(node => node.entry.isFile); - } - return false; - }); - } - } + }); } return false; From ff02c6acc4957ca83576186776f4915dca821b53 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Fri, 13 Jul 2018 04:42:54 +0100 Subject: [PATCH 024/146] extension api: permission fixes (#507) * fix multiple targets * permission fixes * tests and code fixes * remove famous fdescribe * and the missing semicolon of course * even more tests --- src/app/extensions/extension.service.spec.ts | 637 +++++++++++++++++++ src/app/extensions/extension.service.ts | 189 +++--- 2 files changed, 743 insertions(+), 83 deletions(-) create mode 100644 src/app/extensions/extension.service.spec.ts diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts new file mode 100644 index 0000000000..e0819e4c85 --- /dev/null +++ b/src/app/extensions/extension.service.spec.ts @@ -0,0 +1,637 @@ +/*! + * @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 { TestBed } from '@angular/core/testing'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ExtensionService } from './extension.service'; +import { AppConfigService } from '@alfresco/adf-core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../store/states'; +import { ContentActionType } from './content-action.extension'; + +describe('ExtensionService', () => { + let config: AppConfigService; + let extensions: ExtensionService; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule] + }); + + extensions = TestBed.get(ExtensionService); + store = TestBed.get(Store); + + config = TestBed.get(AppConfigService); + config.config['extensions'] = {}; + }); + + describe('auth guards', () => { + let guard1; + let guard2; + + beforeEach(() => { + guard1 = {}; + guard2 = {}; + + extensions.authGuards['guard1'] = guard1; + extensions.authGuards['guard2'] = guard2; + + extensions.init(); + }); + + it('should fetch auth guards by ids', () => { + const guards = extensions.getAuthGuards(['guard2', 'guard1']); + + expect(guards.length).toBe(2); + expect(guards[0]).toEqual(guard1); + expect(guards[1]).toEqual(guard2); + }); + + it('should not fetch auth guards for missing ids', () => { + const guards = extensions.getAuthGuards(null); + expect(guards).toEqual([]); + }); + + it('should fetch only known guards', () => { + const guards = extensions.getAuthGuards(['missing', 'guard1']); + + expect(guards.length).toBe(1); + expect(guards[0]).toEqual(guard1); + }); + }); + + describe('components', () => { + let component1; + + beforeEach(() => { + component1 = {}; + + extensions.components['component-1'] = component1; + extensions.init(); + }); + + it('should fetch registered component', () => { + const component = extensions.getComponentById('component-1'); + expect(component).toEqual(component1); + }); + + it('should not fetch registered component', () => { + const component = extensions.getComponentById('missing'); + expect(component).toBeFalsy(); + }); + }); + + describe('routes', () => { + let component1, component2; + let guard1; + + beforeEach(() => { + config.config.extensions = { + core: { + routes: [ + { + id: 'aca:routes/about', + path: 'ext/about', + component: 'aca:components/about', + layout: 'aca:layouts/main', + auth: ['aca:auth'], + data: { + title: 'Custom About' + } + } + ] + } + }; + + component1 = {}; + component2 = {}; + extensions.components['aca:components/about'] = component1; + extensions.components['aca:layouts/main'] = component2; + + guard1 = {}; + extensions.authGuards['aca:auth'] = guard1; + + extensions.init(); + }); + + it('should load routes from the config', () => { + expect(extensions.routes.length).toBe(1); + }); + + it('should find a route by id', () => { + const route = extensions.getRouteById('aca:routes/about'); + expect(route).toBeTruthy(); + expect(route.path).toBe('ext/about'); + }); + + it('should not find a route by id', () => { + const route = extensions.getRouteById('some-route'); + expect(route).toBeFalsy(); + }); + + it('should build application routes', () => { + const routes = extensions.getApplicationRoutes(); + + expect(routes.length).toBe(1); + + const route = routes[0]; + expect(route.path).toBe('ext/about'); + expect(route.component).toEqual(component2); + expect(route.canActivateChild).toEqual([guard1]); + expect(route.canActivate).toEqual([guard1]); + expect(route.children.length).toBe(1); + expect(route.children[0].path).toBe(''); + expect(route.children[0].component).toEqual(component1); + }); + }); + + describe('actions', () => { + beforeEach(() => { + config.config.extensions = { + core: { + actions: [ + { + id: 'aca:actions/create-folder', + type: 'CREATE_FOLDER', + payload: 'folder-name' + } + ] + } + }; + }); + + it('should load actions from the config', () => { + extensions.init(); + expect(extensions.actions.length).toBe(1); + }); + + it('should have an empty action list if config provides nothing', () => { + config.config.extensions = {}; + extensions.init(); + + expect(extensions.actions).toEqual([]); + }); + + it('should find action by id', () => { + extensions.init(); + + const action = extensions.getActionById( + 'aca:actions/create-folder' + ); + expect(action).toBeTruthy(); + expect(action.type).toBe('CREATE_FOLDER'); + expect(action.payload).toBe('folder-name'); + }); + + it('should not find action by id', () => { + extensions.init(); + + const action = extensions.getActionById('missing'); + expect(action).toBeFalsy(); + }); + + it('should run the action via store', () => { + extensions.init(); + spyOn(store, 'dispatch').and.stub(); + + extensions.runActionById('aca:actions/create-folder'); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'CREATE_FOLDER', + payload: 'folder-name' + }); + }); + + it('should not use store if action is missing', () => { + extensions.init(); + spyOn(store, 'dispatch').and.stub(); + + extensions.runActionById('missing'); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + describe('content actions', () => { + it('should load content actions from the config', () => { + config.config.extensions = { + core: { + features: { + content: { + actions: [ + { + id: 'aca:toolbar/separator-1', + order: 1, + type: 'separator' + }, + { + id: 'aca:toolbar/separator-2', + order: 2, + type: 'separator' + } + ] + } + } + } + }; + + extensions.init(); + expect(extensions.contentActions.length).toBe(2); + }); + + it('should have an empty content action list if config is empty', () => { + config.config.extensions = {}; + extensions.init(); + expect(extensions.contentActions).toEqual([]); + }); + + it('should sort content actions by order', () => { + config.config.extensions = { + core: { + features: { + content: { + actions: [ + { + id: 'aca:toolbar/separator-2', + order: 2, + type: 'separator' + }, + { + id: 'aca:toolbar/separator-1', + order: 1, + type: 'separator' + } + ] + } + } + } + }; + + extensions.init(); + expect(extensions.contentActions.length).toBe(2); + expect(extensions.contentActions[0].id).toBe( + 'aca:toolbar/separator-1' + ); + expect(extensions.contentActions[1].id).toBe( + 'aca:toolbar/separator-2' + ); + }); + }); + + describe('open with', () => { + it('should load [open with] actions for the viewer', () => { + config.config.extensions = { + core: { + features: { + viewer: { + 'open-with': [ + { + disabled: false, + id: 'aca:viewer/action1', + order: 100, + icon: 'build', + title: 'Snackbar', + action: 'aca:actions/info' + } + ] + } + } + } + }; + + extensions.init(); + expect(extensions.openWithActions.length).toBe(1); + }); + + it('should have an empty [open with] list if config is empty', () => { + config.config.extensions = {}; + extensions.init(); + expect(extensions.openWithActions).toEqual([]); + }); + + it('should load only enabled [open with] actions for the viewer', () => { + config.config.extensions = { + core: { + features: { + viewer: { + 'open-with': [ + { + id: 'aca:viewer/action2', + order: 200, + icon: 'build', + title: 'Snackbar', + action: 'aca:actions/info' + }, + { + disabled: true, + id: 'aca:viewer/action1', + order: 100, + icon: 'build', + title: 'Snackbar', + action: 'aca:actions/info' + } + ] + } + } + } + }; + + extensions.init(); + expect(extensions.openWithActions.length).toBe(1); + expect(extensions.openWithActions[0].id).toBe('aca:viewer/action2'); + }); + + it('should sort [open with] actions by order', () => { + config.config.extensions = { + core: { + features: { + viewer: { + 'open-with': [ + { + id: 'aca:viewer/action2', + order: 200, + icon: 'build', + title: 'Snackbar', + action: 'aca:actions/info' + }, + { + id: 'aca:viewer/action1', + order: 100, + icon: 'build', + title: 'Snackbar', + action: 'aca:actions/info' + } + ] + } + } + } + }; + + extensions.init(); + expect(extensions.openWithActions.length).toBe(2); + expect(extensions.openWithActions[0].id).toBe('aca:viewer/action1'); + expect(extensions.openWithActions[1].id).toBe('aca:viewer/action2'); + }); + }); + + describe('create', () => { + it('should load [create] actions from config', () => { + config.config.extensions = { + core: { + features: { + create: [ + { + disabled: false, + id: 'aca:create/folder', + order: 100, + icon: 'create_new_folder', + title: 'ext: Create Folder', + target: { + permissions: ['create'], + action: 'aca:actions/create-folder' + } + } + ] + } + } + }; + + extensions.init(); + expect(extensions.createActions.length).toBe(1); + }); + + it('should have an empty [create] actions if config is empty', () => { + config.config.extensions = {}; + extensions.init(); + expect(extensions.createActions).toEqual([]); + }); + + it('should sort [create] actions by order', () => { + config.config.extensions = { + core: { + features: { + create: [ + { + id: 'aca:create/folder', + order: 100, + icon: 'create_new_folder', + title: 'ext: Create Folder', + target: { + permissions: ['create'], + action: 'aca:actions/create-folder' + } + }, + { + id: 'aca:create/folder-2', + order: 10, + icon: 'create_new_folder', + title: 'ext: Create Folder', + target: { + permissions: ['create'], + action: 'aca:actions/create-folder' + } + } + ] + } + } + }; + + extensions.init(); + expect(extensions.createActions.length).toBe(2); + expect(extensions.createActions[0].id).toBe('aca:create/folder-2'); + expect(extensions.createActions[1].id).toBe('aca:create/folder'); + }); + }); + + describe('expressions', () => { + it('should eval static value', () => { + const value = extensions.runExpression('hello world'); + expect(value).toBe('hello world'); + }); + + it('should eval string as an expression', () => { + const value = extensions.runExpression('$( "hello world" )'); + expect(value).toBe('hello world'); + }); + + it('should eval expression with no context', () => { + const value = extensions.runExpression('$( 1 + 1 )'); + expect(value).toBe(2); + }); + + it('should eval expression with context', () => { + const context = { + a: 'hey', + b: 'there' + }; + const expression = '$( context.a + " " + context.b + "!" )'; + const value = extensions.runExpression(expression, context); + expect(value).toBe('hey there!'); + }); + }); + + describe('permissions', () => { + it('should approve node permission', () => { + const node: any = { + allowableOperations: ['create'] + }; + + expect(extensions.nodeHasPermission(node, 'create')).toBeTruthy(); + }); + + it('should not approve node permission', () => { + const node: any = { + allowableOperations: ['create'] + }; + + expect(extensions.nodeHasPermission(node, 'update')).toBeFalsy(); + }); + + it('should not approve node permission when node missing property', () => { + const node: any = { + allowableOperations: null + }; + + expect(extensions.nodeHasPermission(node, 'update')).toBeFalsy(); + }); + + it('should require node to check permission', () => { + expect(extensions.nodeHasPermission(null, 'create')).toBeFalsy(); + }); + + it('should require permission value to check', () => { + const node: any = { + allowableOperations: ['create'] + }; + expect(extensions.nodeHasPermission(node, null)).toBeFalsy(); + }); + + it('should approve multiple permissions', () => { + const node: any = { + allowableOperations: ['create', 'update', 'delete'] + }; + expect( + extensions.nodeHasPermissions(node, ['create', 'delete']) + ).toBeTruthy(); + }); + + it('should require node to check multiple permissions', () => { + expect(extensions.nodeHasPermissions(null, ['create'])).toBeFalsy(); + }); + + it('should require multiple permissions to check', () => { + const node: any = { + allowableOperations: ['create', 'update', 'delete'] + }; + expect(extensions.nodeHasPermissions(node, null)).toBeFalsy(); + }); + }); + + describe('sorting', () => { + it('should sort by provided order', () => { + const sorted = [ + { id: '1', order: 10 }, + { id: '2', order: 1 }, + { id: '3', order: 5 } + ].sort(extensions.sortByOrder); + + expect(sorted[0].id).toBe('2'); + expect(sorted[1].id).toBe('3'); + expect(sorted[2].id).toBe('1'); + }); + + it('should use implicit order', () => { + const sorted = [ + { id: '3'}, + { id: '2' }, + { id: '1', order: 1 } + ].sort(extensions.sortByOrder); + + expect(sorted[0].id).toBe('1'); + expect(sorted[1].id).toBe('3'); + expect(sorted[2].id).toBe('2'); + }); + }); + + describe('filtering', () => { + it('should filter out disabled items', () => { + const items = [ + { id: 1, disabled: true }, + { id: 2 }, + { id: 3, disabled: true } + ].filter(extensions.filterEnabled); + + expect(items.length).toBe(1); + expect(items[0].id).toBe(2); + }); + }); + + it('should reduce duplicate separators', () => { + const actions = [ + { id: '1', type: ContentActionType.button }, + { id: '2', type: ContentActionType.separator }, + { id: '3', type: ContentActionType.separator }, + { id: '4', type: ContentActionType.separator }, + { id: '5', type: ContentActionType.button } + ]; + + const result = actions.reduce(extensions.reduceSeparators, []); + expect(result.length).toBe(3); + expect(result[0].id).toBe('1'); + expect(result[1].id).toBe('2'); + expect(result[2].id).toBe('5'); + }); + + it('should trim trailing separators', () => { + const actions = [ + { id: '1', type: ContentActionType.button }, + { id: '2', type: ContentActionType.separator } + ]; + + const result = actions.reduce(extensions.reduceSeparators, []); + expect(result.length).toBe(1); + expect(result[0].id).toBe('1'); + }); + + it('should reduce empty menus', () => { + const actions = [ + { id: '1', type: ContentActionType.button }, + { + id: '2', + type: ContentActionType.menu + }, + { + id: '3', + type: ContentActionType.menu, + children: [{ id: '3-1', type: ContentActionType.button }] + } + ]; + + const result = actions.reduce(extensions.reduceEmptyMenus, []); + + expect(result.length).toBe(2); + expect(result[0].id).toBe('1'); + expect(result[1].id).toBe('3'); + }); +}); diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index adcf40f2c7..40e7c10b0d 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -27,7 +27,10 @@ import { Injectable, Type } from '@angular/core'; import { RouteExtension } from './route.extension'; import { ActionExtension } from './action.extension'; import { AppConfigService } from '@alfresco/adf-core'; -import { ContentActionExtension, ContentActionType } from './content-action.extension'; +import { + ContentActionExtension, + ContentActionType +} from './content-action.extension'; import { OpenWithExtension } from './open-with.extension'; import { AppStore } from '../store/states'; import { Store } from '@ngrx/store'; @@ -133,8 +136,10 @@ export class ExtensionService { return []; } - getAuthGuards(ids: string[] = []): Array> { - return ids.map(id => this.authGuards[id]); + getAuthGuards(ids: string[]): Array> { + return (ids || []) + .map(id => this.authGuards[id]) + .filter(guard => guard); } getComponentById(id: string): Type<{}> { @@ -207,15 +212,25 @@ export class ExtensionService { return this.contentActions .filter(this.filterEnabled) .filter(action => this.filterByTarget(nodes, action)) - .filter(action => this.filterByPermission(nodes, action, parentNode)) + .filter(action => + this.filterByPermission(nodes, action, parentNode) + ) .reduce(this.reduceSeparators, []) .map(action => { if (action.type === ContentActionType.menu) { const copy = this.copyAction(action); if (copy.children && copy.children.length > 0) { copy.children = copy.children - .filter(childAction => this.filterByTarget(nodes, childAction)) - .filter(childAction => this.filterByPermission(nodes, childAction, parentNode)) + .filter(childAction => + this.filterByTarget(nodes, childAction) + ) + .filter(childAction => + this.filterByPermission( + nodes, + childAction, + parentNode + ) + ) .reduce(this.reduceSeparators, []); } return copy; @@ -225,66 +240,7 @@ export class ExtensionService { .reduce(this.reduceEmptyMenus, []); } - // todo: support multiple selected nodes - private filterByPermission( - nodes: MinimalNodeEntity[], - action: ContentActionExtension, - parentNode: Node - ): boolean { - if (!action) { - return false; - } - - if (!action.target) { - return action.type === ContentActionType.separator - || action.type === ContentActionType.menu; - } - - const permissions = action.target.permissions; - - if (!permissions || permissions.length === 0) { - return true; - } - - return permissions.some(permission => { - if (permission.startsWith('parent.')) { - if (parentNode) { - const parentQuery = permission.split('.')[1]; - return this.nodeHasPermissions(parentNode, [parentQuery]); - } - return false; - } - - if (nodes && nodes.length > 0) { - return this.nodeHasPermissions( - nodes[0].entry, - permissions - ); - } - - return true; - }); - - return true; - } - - private nodeHasPermissions( - node: Node, - permissions: string[] = [] - ): boolean { - if ( - node && - node.allowableOperations && - node.allowableOperations.length > 0 - ) { - return permissions.some(permission => - node.allowableOperations.includes(permission) - ); - } - return false; - } - - private reduceSeparators( + reduceSeparators( acc: ContentActionExtension[], el: ContentActionExtension, i: number, @@ -299,17 +255,19 @@ export class ExtensionService { ) { return acc; } - } - // remove trailing separator - if (i === arr.length - 1) { - if (el.type === ContentActionType.separator) { - return acc; + + // remove trailing separator + if (i === arr.length - 1) { + if (el.type === ContentActionType.separator) { + return acc; + } } } + return acc.concat(el); } - private reduceEmptyMenus( + reduceEmptyMenus( acc: ContentActionExtension[], el: ContentActionExtension ): ContentActionExtension[] { @@ -321,7 +279,7 @@ export class ExtensionService { return acc.concat(el); } - private sortByOrder( + sortByOrder( a: { order?: number | undefined }, b: { order?: number | undefined } ) { @@ -330,20 +288,20 @@ export class ExtensionService { return left - right; } - private filterEnabled(entry: { disabled?: boolean }): boolean { + filterEnabled(entry: { disabled?: boolean }): boolean { return !entry.disabled; } - private copyAction( - action: ContentActionExtension - ): ContentActionExtension { + copyAction(action: ContentActionExtension): ContentActionExtension { return { ...action, - children: (action.children || []).map(child => this.copyAction(child)) + children: (action.children || []).map(child => + this.copyAction(child) + ) }; } - private filterByTarget( + filterByTarget( nodes: MinimalNodeEntity[], action: ContentActionExtension ): boolean { @@ -358,9 +316,9 @@ export class ExtensionService { ); } - const types = action.target.types; + const types = action.target.types || []; - if (!types || types.length === 0) { + if (types.length === 0) { return true; } @@ -369,12 +327,14 @@ export class ExtensionService { if (type === 'folder') { return action.target.multiple ? nodes.some(node => node.entry.isFolder) - : nodes.every(node => node.entry.isFolder); + : nodes.length === 1 && + nodes.every(node => node.entry.isFolder); } if (type === 'file') { return action.target.multiple ? nodes.some(node => node.entry.isFile) - : nodes.every(node => node.entry.isFile); + : nodes.length === 1 && + nodes.every(node => node.entry.isFile); } return false; }); @@ -382,4 +342,67 @@ export class ExtensionService { return false; } + + filterByPermission( + nodes: MinimalNodeEntity[], + action: ContentActionExtension, + parentNode: Node + ): boolean { + if (!action) { + return false; + } + + if (!action.target) { + return ( + action.type === ContentActionType.separator || + action.type === ContentActionType.menu + ); + } + + const permissions = action.target.permissions || []; + + if (permissions.length === 0) { + return true; + } + + return permissions.some(permission => { + if (permission.startsWith('parent.')) { + if (parentNode) { + const parentQuery = permission.split('.')[1]; + return this.nodeHasPermission(parentNode, parentQuery); + } + return false; + } + + if (nodes && nodes.length > 0) { + return action.target.multiple + ? nodes.some(node => + this.nodeHasPermission(node.entry, permission) + ) + : nodes.length === 1 && + nodes.every(node => + this.nodeHasPermission(node.entry, permission) + ); + } + + return false; + }); + } + + nodeHasPermissions(node: Node, permissions: string[]): boolean { + if (node && permissions && permissions.length > 0) { + return permissions.some(permission => + this.nodeHasPermission(node, permission) + ); + } + return false; + } + + nodeHasPermission(node: Node, permission: string): boolean { + if (node && permission) { + const allowableOperations = node.allowableOperations || []; + return allowableOperations.includes(permission); + } + return false; + } } From 19021c8b5137f691b13005fd94faf3a3972eab66 Mon Sep 17 00:00:00 2001 From: Suzana Dirla Date: Fri, 13 Jul 2018 08:35:34 +0300 Subject: [PATCH 025/146] [ACA-1545] upgrade to latest ADF alpha (#505) * [ACA-1545] upgrade to latest ADF alpha * [ACA-1545] package-json generated using node v10.6.0 --- package-lock.json | 34 +++++++--------------------------- package.json | 4 ++-- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfb887f542..37c11f1f18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,11 +5,11 @@ "requires": true, "dependencies": { "@alfresco/adf-content-services": { - "version": "2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec", - "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec.tgz", - "integrity": "sha512-mhQpgadbL2OFv94mLpcNjuF6Ga2fMhpozBzmtb1SBx2tNe/ZrmX7EDxo+A8vXjQgUUyrnxj3Tw6Zayp7DXK7lQ==", + "version": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", + "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3.tgz", + "integrity": "sha512-qIYgXgIptndkAjVmVzWXRRp5RQA4S0zcOACBrStKDkivAS+WxXDVNN/ZVpT9hM8LucpniIaInTc9W7xSX4Irrw==", "requires": { - "@alfresco/adf-core": "2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec", + "@alfresco/adf-core": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", "@angular/common": "5.1.1", @@ -40,16 +40,6 @@ "zone.js": "0.8.14" }, "dependencies": { - "alfresco-js-api": { - "version": "2.5.0-3a53a7c2417f4e004631e5b5c76097cba312a714", - "resolved": "https://registry.npmjs.org/alfresco-js-api/-/alfresco-js-api-2.5.0-3a53a7c2417f4e004631e5b5c76097cba312a714.tgz", - "integrity": "sha512-hibbA/ziZuJ28xUnwyLbddsdn+idDHSSedonD0rzhuEsM3NN4SvHuQMSsHfssEDDQztKfYsWtjo5SjCjy+9JPA==", - "requires": { - "event-emitter": "0.3.4", - "jsrsasign": "^8.0.12", - "superagent": "3.8.2" - } - }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", @@ -71,9 +61,9 @@ } }, "@alfresco/adf-core": { - "version": "2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec", - "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec.tgz", - "integrity": "sha512-OVDRg4ps/sTGooLzQ2Sr/uG0F3ZJME8A8naAhMF6Errjin3L6Jjo/hbP7hBCjMB+V8dv3GBGLOvhoxUr29yzog==", + "version": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", + "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3.tgz", + "integrity": "sha512-CsppN0mzq3tOrIe6H2XRCr8EtKrvhQ0TvxhV4lDn7kF2jztLOgVm6I6Gyjs7nTMqXecP/WXzkpTkU/stuJOQJA==", "requires": { "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", @@ -105,16 +95,6 @@ "zone.js": "0.8.14" }, "dependencies": { - "alfresco-js-api": { - "version": "2.5.0-3a53a7c2417f4e004631e5b5c76097cba312a714", - "resolved": "https://registry.npmjs.org/alfresco-js-api/-/alfresco-js-api-2.5.0-3a53a7c2417f4e004631e5b5c76097cba312a714.tgz", - "integrity": "sha512-hibbA/ziZuJ28xUnwyLbddsdn+idDHSSedonD0rzhuEsM3NN4SvHuQMSsHfssEDDQztKfYsWtjo5SjCjy+9JPA==", - "requires": { - "event-emitter": "0.3.4", - "jsrsasign": "^8.0.12", - "superagent": "3.8.2" - } - }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", diff --git a/package.json b/package.json index b6c7ed07b6..7b57f39afe 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ }, "private": true, "dependencies": { - "@alfresco/adf-content-services": "2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec", - "@alfresco/adf-core": "2.5.0-834415c1ac82cd124efb0ae1b754bfe447a892ec", + "@alfresco/adf-content-services": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", + "@alfresco/adf-core": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", "@angular/common": "5.1.1", From 0504b28b3c600dabc6db9f212873a333c8c12fa9 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 13 Jul 2018 10:12:03 +0300 Subject: [PATCH 026/146] [ACA-1545] Library - create (#506) * create site implementation * lint * update validation * reuse existent service and renamed site to library --- src/app/app.module.ts | 3 + .../services/content-management.service.ts | 20 ++- .../libraries/libraries.component.html | 8 + .../libraries/libraries.component.ts | 9 +- src/app/dialogs/library/form.validators.ts | 51 +++++++ src/app/dialogs/library/library.dialog.html | 79 ++++++++++ src/app/dialogs/library/library.dialog.scss | 20 +++ src/app/dialogs/library/library.dialog.ts | 140 ++++++++++++++++++ src/app/services/content-api.service.ts | 16 +- src/app/store/actions/library.actions.ts | 6 + src/app/store/effects/library.effects.ts | 57 ++++--- src/assets/i18n/en.json | 26 ++++ 12 files changed, 413 insertions(+), 22 deletions(-) create mode 100644 src/app/dialogs/library/form.validators.ts create mode 100644 src/app/dialogs/library/library.dialog.html create mode 100644 src/app/dialogs/library/library.dialog.scss create mode 100644 src/app/dialogs/library/library.dialog.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bc9b1f848..09e65bc1c4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -61,6 +61,7 @@ import { NodePermanentDeleteDirective } from './common/directives/node-permanent import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; import { NodeVersionsDirective } from './common/directives/node-versions.directive'; import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog'; +import { LibraryDialogComponent } from './dialogs/library/library.dialog'; import { ContentManagementService } from './common/services/content-management.service'; import { NodeActionsService } from './common/services/node-actions.service'; import { NodePermissionService } from './common/services/node-permission.service'; @@ -132,6 +133,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per NodeVersionsDirective, NodePermissionsDirective, NodeVersionsDialogComponent, + LibraryDialogComponent, NodePermissionsDialogComponent, PermissionsManagerComponent, SearchResultsComponent, @@ -162,6 +164,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per ExtensionService ], entryComponents: [ + LibraryDialogComponent, NodeVersionsDialogComponent, NodePermissionsDialogComponent ], diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index fd5314106e..ecba584279 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -27,6 +27,7 @@ import { Subject } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material'; import { FolderDialogComponent } from '@alfresco/adf-content-services'; +import { LibraryDialogComponent } from '../../dialogs/library/library.dialog'; import { SnackbarErrorAction } from '../../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states'; @@ -45,7 +46,8 @@ export class ContentManagementService { nodesRestored = new Subject(); folderEdited = new Subject(); folderCreated = new Subject(); - siteDeleted = new Subject(); + libraryDeleted = new Subject(); + libraryCreated = new Subject(); linksUnshared = new Subject(); constructor( @@ -98,6 +100,22 @@ export class ContentManagementService { }); } + createLibrary() { + const dialogInstance = this.dialogRef.open(LibraryDialogComponent, { + width: '400px' + }); + + dialogInstance.componentInstance.error.subscribe(message => { + this.store.dispatch(new SnackbarErrorAction(message)); + }); + + dialogInstance.afterClosed().subscribe(node => { + if (node) { + this.libraryCreated.next(node); + } + }); + } + canDeleteNode(node: MinimalNodeEntity | Node): boolean { return this.permission.check(node, ['delete']); } diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index 85900e602f..932eef78fc 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -12,6 +12,14 @@ list + + + + + diff --git a/src/app/dialogs/library/library.dialog.scss b/src/app/dialogs/library/library.dialog.scss new file mode 100644 index 0000000000..a27ebb62e3 --- /dev/null +++ b/src/app/dialogs/library/library.dialog.scss @@ -0,0 +1,20 @@ + +.mat-radio-group { + display: flex; + flex-direction: column; + margin: 0 0 20px 0; +} + +.mat-radio-group .mat-radio-button { + margin: 10px 0; +} + +.mat-input-container { + width: 100%; +} + +.actions-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; +} diff --git a/src/app/dialogs/library/library.dialog.ts b/src/app/dialogs/library/library.dialog.ts new file mode 100644 index 0000000000..e33ec4b099 --- /dev/null +++ b/src/app/dialogs/library/library.dialog.ts @@ -0,0 +1,140 @@ +/*! + * @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 { Observable } from 'rxjs/Observable'; +import { Component, OnInit, Output, EventEmitter } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatDialogRef } from '@angular/material'; +import { SiteBody } from 'alfresco-js-api'; +import { ContentApiService } from '../../services/content-api.service'; +import { SiteIdValidator, forbidSpecialCharacters } from './form.validators'; + + +@Component({ + selector: 'app-library-dialog', + styleUrls: ['./library.dialog.scss'], + templateUrl: './library.dialog.html' +}) +export class LibraryDialogComponent implements OnInit { + @Output() + error: EventEmitter = new EventEmitter(); + + @Output() + success: EventEmitter = new EventEmitter(); + + createTitle = 'LIBRARY.DIALOG.CREATE_TITLE'; + form: FormGroup; + visibilityOption: any; + visibilityOptions = [ + { value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false }, + { value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false }, + { value: 'MODERATED', label: 'LIBRARY.VISIBILITY.MODERATED', disabled: false } + ]; + + constructor( + private formBuilder: FormBuilder, + private dialog: MatDialogRef, + private contentApi: ContentApiService + ) {} + + ngOnInit() { + const validators = { + id: [ Validators.required, Validators.maxLength(72), forbidSpecialCharacters ], + title: [ Validators.required, Validators.maxLength(256) ], + description: [ Validators.maxLength(512) ] + }; + + this.form = this.formBuilder.group({ + title: ['', validators.title ], + id: [ '', validators.id, SiteIdValidator.createValidator(this.contentApi) ], + description: [ '', validators.description ], + }); + + this.visibilityOption = this.visibilityOptions[0].value; + + this.form.controls['title'].valueChanges + .debounceTime(300) + .subscribe((titleValue: string) => { + if (!titleValue.trim().length) { + return; + } + + if (!this.form.controls['id'].dirty) { + this.form.patchValue({ id: this.sanitize(titleValue.trim()) }); + this.form.controls['id'].markAsTouched(); + } + }); + } + + get title(): string { + const { title } = this.form.value; + + return (title || '').trim(); + } + + get id(): string { + const { id } = this.form.value; + + return (id || '').trim(); + } + + get description(): string { + const { description } = this.form.value; + + return (description || '').trim(); + } + + get visibility(): string { + return this.visibilityOption || ''; + } + + submit() { + const { form, dialog } = this; + + if (!form.valid) { return; } + + this.create().subscribe( + (folder: any) => { + this.success.emit(folder); + dialog.close(folder); + }, + (error) => this.error.emit('LIBRARY.ERRORS.GENERIC') + ); + } + + visibilityChangeHandler(event) { + this.visibilityOption = event.value; + } + + private create(): Observable { + const { contentApi, title, id, description, visibility } = this; + const siteBody = { + id, + title, + description, + visibility + }; + + return contentApi.createSite(siteBody); + } + + private sanitize(input: string) { + return input + .replace(/[\s]/g, '-') + .replace(/[^A-Za-z0-9-]/g, ''); + } +} diff --git a/src/app/services/content-api.service.ts b/src/app/services/content-api.service.ts index a5ecff227a..992997678a 100644 --- a/src/app/services/content-api.service.ts +++ b/src/app/services/content-api.service.ts @@ -37,7 +37,9 @@ import { FavoritePaging, SharedLinkPaging, SearchRequest, - ResultSetPaging + ResultSetPaging, + SiteBody, + SiteEntry } from 'alfresco-js-api'; @Injectable() @@ -226,4 +228,16 @@ export class ContentApiService { this.api.sitesApi.deleteSite(siteId, opts) ); } + + createSite(siteBody: SiteBody, opts?: {skipConfiguration?: boolean, skipAddToFavorites?: boolean}): Observable { + return Observable.fromPromise( + this.api.sitesApi.createSite(siteBody, opts) + ); + } + + getSite(siteId?: string, opts?: { relations?: Array, fields?: Array }): Observable { + return Observable.fromPromise( + this.api.sitesApi.getSite(siteId, opts) + ); + } } diff --git a/src/app/store/actions/library.actions.ts b/src/app/store/actions/library.actions.ts index d2fad4aef3..bc65400285 100644 --- a/src/app/store/actions/library.actions.ts +++ b/src/app/store/actions/library.actions.ts @@ -26,8 +26,14 @@ import { Action } from '@ngrx/store'; export const DELETE_LIBRARY = 'DELETE_LIBRARY'; +export const CREATE_LIBRARY = 'CREATE_LIBRARY'; export class DeleteLibraryAction implements Action { readonly type = DELETE_LIBRARY; constructor(public payload: string) {} } + +export class CreateLibraryAction implements Action { + readonly type = CREATE_LIBRARY; + constructor() {} +} diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index a8df0f9bbb..115846e16f 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -26,7 +26,10 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { Injectable } from '@angular/core'; import { map } from 'rxjs/operators'; -import { DeleteLibraryAction, DELETE_LIBRARY } from '../actions'; +import { + DeleteLibraryAction, DELETE_LIBRARY, + CreateLibraryAction, CREATE_LIBRARY +} from '../actions'; import { SnackbarInfoAction, SnackbarErrorAction @@ -49,23 +52,41 @@ export class SiteEffects { deleteLibrary$ = this.actions$.pipe( ofType(DELETE_LIBRARY), map(action => { - this.contentApi.deleteSite(action.payload).subscribe( - () => { - this.content.siteDeleted.next(action.payload); - this.store.dispatch( - new SnackbarInfoAction( - 'APP.MESSAGES.INFO.LIBRARY_DELETED' - ) - ); - }, - () => { - this.store.dispatch( - new SnackbarErrorAction( - 'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED' - ) - ); - } - ); + if (action.payload) { + this.deleteLibrary(action.payload); + } + }) + ); + + @Effect({ dispatch: false }) + createLibrary$ = this.actions$.pipe( + ofType(CREATE_LIBRARY), + map(action => { + this.createLibrary(); }) ); + + private deleteLibrary(id: string) { + this.contentApi.deleteSite(id).subscribe( + () => { + this.content.libraryDeleted.next(id); + this.store.dispatch( + new SnackbarInfoAction( + 'APP.MESSAGES.INFO.LIBRARY_DELETED' + ) + ); + }, + () => { + this.store.dispatch( + new SnackbarErrorAction( + 'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED' + ) + ); + } + ); + } + + private createLibrary() { + this.content.createLibrary(); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b3c2fa9b69..62d61ab4d4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -269,6 +269,32 @@ "NO_PERMISSION": "You don't have permission to manage the versions of this content." } }, + "LIBRARY": { + "DIALOG": { + "CREATE_TITLE": "Create Site", + "CREATE": "Create", + "CANCEL": "Cancel", + "FORM": { + "DESCRIPTION": "Description", + "SITE_ID": "Site ID", + "NAME": "Name" + } + }, + "VISIBILITY": { + "PRIVATE": "Private", + "PUBLIC": "Public", + "MODERATED": "Moderated" + }, + "ERRORS": { + "GENERIC": "There was an error", + "EXISTENT_SITE": "ID already used (it might be in the trashcan).", + "ID_TOO_LONG": "Use 72 characters or less for the URL name", + "DESCRIPTION_TOO_LONG": "Use 512 characters or less for description", + "TITLE_TOO_LONG": "Use 256 characters or less for title", + "ILLEGAL_CHARACTERS": "Use characters a-z, A-Z, 0-9 and - only" + } + }, + "SEARCH": { "SORT": { "RELEVANCE": "Relevance", From 53c2e886890f6bdc8fa1f1800aea7fb2a7f8c94e Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 13 Jul 2018 22:22:26 +0300 Subject: [PATCH 027/146] notify library ID conflict (#509) --- src/app/dialogs/library/library.dialog.ts | 12 +++++++++++- src/assets/i18n/en.json | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/dialogs/library/library.dialog.ts b/src/app/dialogs/library/library.dialog.ts index e33ec4b099..3fb0713198 100644 --- a/src/app/dialogs/library/library.dialog.ts +++ b/src/app/dialogs/library/library.dialog.ts @@ -112,7 +112,7 @@ export class LibraryDialogComponent implements OnInit { this.success.emit(folder); dialog.close(folder); }, - (error) => this.error.emit('LIBRARY.ERRORS.GENERIC') + (error) => this.handleError(error) ); } @@ -137,4 +137,14 @@ export class LibraryDialogComponent implements OnInit { .replace(/[\s]/g, '-') .replace(/[^A-Za-z0-9-]/g, ''); } + + private handleError(error: any): any { + const { error: { statusCode } } = JSON.parse(error.message); + + if (statusCode === 409) { + this.form.controls['id'].setErrors({ message: 'LIBRARY.ERRORS.CONFLICT' }); + } + + return error; + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 62d61ab4d4..2d6155afd2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -287,7 +287,8 @@ }, "ERRORS": { "GENERIC": "There was an error", - "EXISTENT_SITE": "ID already used (it might be in the trashcan).", + "EXISTENT_SITE": "This Site ID isn't available. Try a different one", + "CONFLICT": "ID already used (it might be in the trashcan).", "ID_TOO_LONG": "Use 72 characters or less for the URL name", "DESCRIPTION_TOO_LONG": "Use 512 characters or less for description", "TITLE_TOO_LONG": "Use 256 characters or less for title", From d5763f585db701ee0f7b9ee7dfbd7c53341f8f0a Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Sat, 14 Jul 2018 13:46:40 +0300 Subject: [PATCH 028/146] [ACA-1571] File library - navigate into library after create process (#510) * notify library ID conflict * navigate into library node --- src/app/common/services/content-management.service.ts | 7 ++++--- src/app/components/libraries/libraries.component.ts | 4 +++- src/app/dialogs/library/library.dialog.ts | 11 ++++++----- src/app/services/content-api.service.ts | 4 +++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index ecba584279..8dab2870fb 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -34,7 +34,8 @@ import { AppStore } from '../../store/states'; import { MinimalNodeEntity, MinimalNodeEntryEntity, - Node + Node, + SiteEntry } from 'alfresco-js-api'; import { NodePermissionService } from './node-permission.service'; @@ -47,7 +48,7 @@ export class ContentManagementService { folderEdited = new Subject(); folderCreated = new Subject(); libraryDeleted = new Subject(); - libraryCreated = new Subject(); + libraryCreated = new Subject(); linksUnshared = new Subject(); constructor( @@ -109,7 +110,7 @@ export class ContentManagementService { this.store.dispatch(new SnackbarErrorAction(message)); }); - dialogInstance.afterClosed().subscribe(node => { + dialogInstance.afterClosed().subscribe((node: SiteEntry) => { if (node) { this.libraryCreated.next(node); } diff --git a/src/app/components/libraries/libraries.component.ts b/src/app/components/libraries/libraries.component.ts index 7982bb47ff..0aec560108 100644 --- a/src/app/components/libraries/libraries.component.ts +++ b/src/app/components/libraries/libraries.component.ts @@ -55,7 +55,9 @@ export class LibrariesComponent extends PageComponent implements OnInit { this.subscriptions.push( this.content.libraryDeleted.subscribe(() => this.reload()), - this.content.libraryCreated.subscribe(() => this.reload()) + this.content.libraryCreated.subscribe((node: SiteEntry) => { + this.navigate(node.entry.guid); + }) ); } diff --git a/src/app/dialogs/library/library.dialog.ts b/src/app/dialogs/library/library.dialog.ts index 3fb0713198..eea51282e3 100644 --- a/src/app/dialogs/library/library.dialog.ts +++ b/src/app/dialogs/library/library.dialog.ts @@ -19,7 +19,7 @@ import { Observable } from 'rxjs/Observable'; import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef } from '@angular/material'; -import { SiteBody } from 'alfresco-js-api'; +import { SiteBody, SiteEntry } from 'alfresco-js-api'; import { ContentApiService } from '../../services/content-api.service'; import { SiteIdValidator, forbidSpecialCharacters } from './form.validators'; @@ -108,9 +108,10 @@ export class LibraryDialogComponent implements OnInit { if (!form.valid) { return; } this.create().subscribe( - (folder: any) => { - this.success.emit(folder); - dialog.close(folder); + (node: SiteEntry) => { + + this.success.emit(node); + dialog.close(node); }, (error) => this.handleError(error) ); @@ -120,7 +121,7 @@ export class LibraryDialogComponent implements OnInit { this.visibilityOption = event.value; } - private create(): Observable { + private create(): Observable { const { contentApi, title, id, description, visibility } = this; const siteBody = { id, diff --git a/src/app/services/content-api.service.ts b/src/app/services/content-api.service.ts index 992997678a..a864518152 100644 --- a/src/app/services/content-api.service.ts +++ b/src/app/services/content-api.service.ts @@ -229,7 +229,9 @@ export class ContentApiService { ); } - createSite(siteBody: SiteBody, opts?: {skipConfiguration?: boolean, skipAddToFavorites?: boolean}): Observable { + createSite( + siteBody: SiteBody, + opts?: {fields?: Array, skipConfiguration?: boolean, skipAddToFavorites?: boolean}): Observable { return Observable.fromPromise( this.api.sitesApi.createSite(siteBody, opts) ); From 51af2071c28704699422b762c187cc02a73f9a0b Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Mon, 16 Jul 2018 11:27:27 +0100 Subject: [PATCH 029/146] extensibility: rules engine (#511) * rules format prototype * config container * lightweight rules * fdescribe * basic rule integration * migrate "create folder" to click actions * migrate toolbar to new action handlers * rule support for "create folder" (toolbar) * upgrade "View" toolbar command * migrate to rules * cleanup tests --- src/app.config.json | 93 +++++----- src/app/app.module.ts | 4 +- src/app/components/page.component.ts | 3 +- .../components/sidenav/sidenav.component.html | 2 +- .../toolbar-action.component.html | 4 +- .../extensions/content-action.extension.ts | 13 +- src/app/extensions/extension.config.ts | 35 ++++ src/app/extensions/extension.service.spec.ts | 57 ------- src/app/extensions/extension.service.ts | 159 +++--------------- src/app/extensions/rules/app.evaluators.ts | 72 ++++++++ src/app/extensions/rules/core.evaluators.ts | 47 ++++++ src/app/extensions/rules/rule-context.ts | 34 ++++ src/app/extensions/rules/rule-parameter.ts | 29 ++++ src/app/extensions/rules/rule-ref.ts | 34 ++++ src/app/extensions/rules/rule.service.ts | 97 +++++++++++ src/app/store/selectors/app.selectors.ts | 1 + src/app/testing/app-testing.module.ts | 4 +- 17 files changed, 441 insertions(+), 247 deletions(-) create mode 100644 src/app/extensions/extension.config.ts create mode 100644 src/app/extensions/rules/app.evaluators.ts create mode 100644 src/app/extensions/rules/core.evaluators.ts create mode 100644 src/app/extensions/rules/rule-context.ts create mode 100644 src/app/extensions/rules/rule-parameter.ts create mode 100644 src/app/extensions/rules/rule-ref.ts create mode 100644 src/app/extensions/rules/rule.service.ts diff --git a/src/app.config.json b/src/app.config.json index 39041e5ec0..bbccd407a3 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -42,6 +42,28 @@ "plugin2.json" ], "core": { + "rules": [ + { + "id": "app.create.canCreateFolder", + "type": "app.navigation.folder.canCreate" + }, + { + "id": "app.toolbar.canEditFolder", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.folder" }, + { "type": "rule", "value": "app.selection.folder.canUpdate" } + ] + }, + { + "id": "app.toolbar.canViewFile", + "type": "app.selection.file" + }, + { + "id": "app.toolbar.canDownload", + "type": "app.selection.canDownload" + } + ], "routes": [ { "id": "aca:routes/about", @@ -81,11 +103,6 @@ "type": "SNACKBAR_INFO", "payload": "I'm a nice little popup raised by extension." }, - { - "id": "aca:actions/error", - "type": "SNACKBAR_ERROR", - "payload": "Aw, Snap!" - }, { "id": "aca:actions/node-name", "type": "SNACKBAR_INFO", @@ -100,14 +117,14 @@ "features": { "create": [ { - "disabled": false, - "id": "aca:create/folder", - "order": 100, + "id": "app.create.folder", "icon": "create_new_folder", "title": "ext: Create Folder", - "target": { - "permissions": ["create"], - "action": "aca:actions/create-folder" + "actions": { + "click": "aca:actions/create-folder" + }, + "rules": { + "enabled": "app.create.canCreateFolder" } } ], @@ -210,10 +227,11 @@ "order": 10, "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", "icon": "create_new_folder", - "target": { - "types": [], - "permissions": ["parent.create"], - "action": "aca:actions/create-folder" + "actions": { + "click": "aca:actions/create-folder" + }, + "rules": { + "visible": "app.create.canCreateFolder" } }, { @@ -222,10 +240,11 @@ "order": 15, "title": "APP.ACTIONS.VIEW", "icon": "open_in_browser", - "target": { - "types": ["file"], - "permissions": [], - "action": "aca:actions/preview" + "actions": { + "click": "aca:actions/preview" + }, + "rules": { + "visible": "app.toolbar.canViewFile" } }, { @@ -234,11 +253,11 @@ "order": 20, "title": "APP.ACTIONS.DOWNLOAD", "icon": "get_app", - "target": { - "types": ["file", "folder"], - "permissions": [], - "action": "aca:actions/download", - "multiple": true + "actions": { + "click": "aca:actions/download" + }, + "rules": { + "visible": "app.toolbar.canDownload" } }, { @@ -247,10 +266,11 @@ "order": 30, "title": "APP.ACTIONS.EDIT", "icon": "create", - "target": { - "types": ["folder"], - "permissions": ["update"], - "action": "aca:actions/edit-folder" + "actions": { + "click": "aca:actions/edit-folder" + }, + "rules": { + "visible": "app.toolbar.canEditFolder" } }, @@ -270,21 +290,8 @@ "type": "button", "title": "Settings", "icon": "settings_applications", - "target": { - "types": [], - "permissions": [], - "action": "aca:actions/settings" - } - }, - { - "id": "aca:action4", - "type": "button", - "title": "Error", - "icon": "report_problem", - "target": { - "types": ["file"], - "permissions": ["update", "delete"], - "action": "aca:actions/error" + "actions": { + "click": "aca:actions/settings" } } ] diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 09e65bc1c4..2bf23b8413 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -85,6 +85,7 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; import { NodePermissionsDirective } from './common/directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; +import { RuleService } from './extensions/rules/rule.service'; @NgModule({ imports: [ @@ -161,7 +162,8 @@ import { PermissionsManagerComponent } from './components/permission-manager/per ProfileResolver, ExperimentalGuard, ContentApiService, - ExtensionService + ExtensionService, + RuleService ], entryComponents: [ LibraryDialogComponent, diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index e84c547a0f..95c3dd9098 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -83,8 +83,7 @@ export abstract class PageComponent implements OnInit, OnDestroy { if (selection.isEmpty) { this.infoDrawerOpened = false; } - const selectedNodes = selection ? selection.nodes : null; - this.actions = this.extensions.getAllowedContentActions(selectedNodes, this.node); + this.actions = this.extensions.getAllowedContentActions(); this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file); this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first); this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes); diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 851a0b6b4e..042d0b5ba3 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -10,7 +10,7 @@ diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html index c4e03c64d3..9684bf899b 100644 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.html +++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.html @@ -3,7 +3,7 @@ mat-icon-button color="primary" title="{{ entry.title | translate }}" - (click)="runAction(entry.target.action)"> + (click)="runAction(entry.actions.click)"> {{ entry.icon }} @@ -20,7 +20,7 @@ [overlapTrigger]="false"> diff --git a/src/app/extensions/content-action.extension.ts b/src/app/extensions/content-action.extension.ts index 0f4478c2dc..65e805de7c 100644 --- a/src/app/extensions/content-action.extension.ts +++ b/src/app/extensions/content-action.extension.ts @@ -38,10 +38,13 @@ export interface ContentActionExtension { icon?: string; disabled?: boolean; children?: Array; - target: { - types: Array; - permissions: Array, - action: string; - multiple?: boolean; + actions?: { + click?: string; + [key: string]: string; + }; + rules: { + enabled?: string; + visible?: string; + [key: string]: string; }; } diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts new file mode 100644 index 0000000000..0a5e9af205 --- /dev/null +++ b/src/app/extensions/extension.config.ts @@ -0,0 +1,35 @@ +/*! + * @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 { RouteExtension } from './route.extension'; +import { ActionExtension } from './action.extension'; +import { RuleRef } from './rules/rule-ref'; + +export interface ExtensionConfig { + rules?: Array; + routes?: Array; + actions?: Array; + features?: { [key: string]: any }; +} diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts index e0819e4c85..fa1e27278d 100644 --- a/src/app/extensions/extension.service.spec.ts +++ b/src/app/extensions/extension.service.spec.ts @@ -491,63 +491,6 @@ describe('ExtensionService', () => { }); }); - describe('permissions', () => { - it('should approve node permission', () => { - const node: any = { - allowableOperations: ['create'] - }; - - expect(extensions.nodeHasPermission(node, 'create')).toBeTruthy(); - }); - - it('should not approve node permission', () => { - const node: any = { - allowableOperations: ['create'] - }; - - expect(extensions.nodeHasPermission(node, 'update')).toBeFalsy(); - }); - - it('should not approve node permission when node missing property', () => { - const node: any = { - allowableOperations: null - }; - - expect(extensions.nodeHasPermission(node, 'update')).toBeFalsy(); - }); - - it('should require node to check permission', () => { - expect(extensions.nodeHasPermission(null, 'create')).toBeFalsy(); - }); - - it('should require permission value to check', () => { - const node: any = { - allowableOperations: ['create'] - }; - expect(extensions.nodeHasPermission(node, null)).toBeFalsy(); - }); - - it('should approve multiple permissions', () => { - const node: any = { - allowableOperations: ['create', 'update', 'delete'] - }; - expect( - extensions.nodeHasPermissions(node, ['create', 'delete']) - ).toBeTruthy(); - }); - - it('should require node to check multiple permissions', () => { - expect(extensions.nodeHasPermissions(null, ['create'])).toBeFalsy(); - }); - - it('should require multiple permissions to check', () => { - const node: any = { - allowableOperations: ['create', 'update', 'delete'] - }; - expect(extensions.nodeHasPermissions(node, null)).toBeFalsy(); - }); - }); - describe('sorting', () => { it('should sort by provided order', () => { const sorted = [ diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 40e7c10b0d..6991c81fa4 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -36,7 +36,8 @@ import { AppStore } from '../store/states'; import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; -import { Node, MinimalNodeEntity } from 'alfresco-js-api'; +import { Node } from 'alfresco-js-api'; +import { RuleService } from './rules/rule.service'; @Injectable() export class ExtensionService { @@ -52,7 +53,8 @@ export class ExtensionService { constructor( private config: AppConfigService, - private store: Store + private store: Store, + private ruleService: RuleService ) {} // initialise extension service @@ -89,6 +91,8 @@ export class ExtensionService { [] ) .sort(this.sortByOrder); + + this.ruleService.init(); } getRouteById(id: string): RouteExtension { @@ -183,38 +187,28 @@ export class ExtensionService { // evaluates create actions for the folder node getFolderCreateActions(folder: Node): Array { - return this.createActions.filter(this.filterEnabled).map(action => { - if ( - action.target && - action.target.permissions && - action.target.permissions.length > 0 - ) { + return this.createActions + .filter(this.filterEnabled) + .filter(action => this.filterByRules(action)) + .map(action => { + let disabled = false; + + if (action.rules && action.rules.enabled) { + disabled = !this.ruleService.evaluateRule(action.rules.enabled); + } + return { ...action, - disabled: !this.nodeHasPermissions( - folder, - action.target.permissions - ), - target: { - ...action.target - } + disabled }; - } - return action; }); } // evaluates content actions for the selection and parent folder node - getAllowedContentActions( - nodes: MinimalNodeEntity[], - parentNode: Node - ): Array { + getAllowedContentActions(): Array { return this.contentActions .filter(this.filterEnabled) - .filter(action => this.filterByTarget(nodes, action)) - .filter(action => - this.filterByPermission(nodes, action, parentNode) - ) + .filter(action => this.filterByRules(action)) .reduce(this.reduceSeparators, []) .map(action => { if (action.type === ContentActionType.menu) { @@ -222,14 +216,7 @@ export class ExtensionService { if (copy.children && copy.children.length > 0) { copy.children = copy.children .filter(childAction => - this.filterByTarget(nodes, childAction) - ) - .filter(childAction => - this.filterByPermission( - nodes, - childAction, - parentNode - ) + this.filterByRules(childAction) ) .reduce(this.reduceSeparators, []); } @@ -301,108 +288,10 @@ export class ExtensionService { }; } - filterByTarget( - nodes: MinimalNodeEntity[], - action: ContentActionExtension - ): boolean { - if (!action) { - return false; - } - - if (!action.target) { - return ( - action.type === ContentActionType.separator || - action.type === ContentActionType.menu - ); - } - - const types = action.target.types || []; - - if (types.length === 0) { - return true; - } - - if (nodes && nodes.length > 0) { - return types.some(type => { - if (type === 'folder') { - return action.target.multiple - ? nodes.some(node => node.entry.isFolder) - : nodes.length === 1 && - nodes.every(node => node.entry.isFolder); - } - if (type === 'file') { - return action.target.multiple - ? nodes.some(node => node.entry.isFile) - : nodes.length === 1 && - nodes.every(node => node.entry.isFile); - } - return false; - }); - } - - return false; - } - - filterByPermission( - nodes: MinimalNodeEntity[], - action: ContentActionExtension, - parentNode: Node - ): boolean { - if (!action) { - return false; - } - - if (!action.target) { - return ( - action.type === ContentActionType.separator || - action.type === ContentActionType.menu - ); - } - - const permissions = action.target.permissions || []; - - if (permissions.length === 0) { - return true; - } - - return permissions.some(permission => { - if (permission.startsWith('parent.')) { - if (parentNode) { - const parentQuery = permission.split('.')[1]; - return this.nodeHasPermission(parentNode, parentQuery); - } - return false; - } - - if (nodes && nodes.length > 0) { - return action.target.multiple - ? nodes.some(node => - this.nodeHasPermission(node.entry, permission) - ) - : nodes.length === 1 && - nodes.every(node => - this.nodeHasPermission(node.entry, permission) - ); - } - - return false; - }); - } - - nodeHasPermissions(node: Node, permissions: string[]): boolean { - if (node && permissions && permissions.length > 0) { - return permissions.some(permission => - this.nodeHasPermission(node, permission) - ); - } - return false; - } - - nodeHasPermission(node: Node, permission: string): boolean { - if (node && permission) { - const allowableOperations = node.allowableOperations || []; - return allowableOperations.includes(permission); + filterByRules(action: ContentActionExtension): boolean { + if (action && action.rules && action.rules.visible) { + return this.ruleService.evaluateRule(action.rules.visible); } - return false; + return true; } } diff --git a/src/app/extensions/rules/app.evaluators.ts b/src/app/extensions/rules/app.evaluators.ts new file mode 100644 index 0000000000..51a7560f53 --- /dev/null +++ b/src/app/extensions/rules/app.evaluators.ts @@ -0,0 +1,72 @@ +/*! + * @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 { RuleContext } from './rule-context'; +import { RuleParameter } from './rule-parameter'; +import { Node } from 'alfresco-js-api'; + +export function canCreateFolder(context: RuleContext, ...args: RuleParameter[]): boolean { + const folder = context.navigation.currentFolder; + if (folder) { + return nodeHasPermission(folder, 'create'); + } + return false; +} + +export function canDownloadSelection(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!context.selection.isEmpty) { + return context.selection.nodes.every(node => { + return node.entry && (node.entry.isFile || node.entry.isFolder); + }); + } + return false; + +} + +export function hasFolderSelected(context: RuleContext, ...args: RuleParameter[]): boolean { + const folder = context.selection.folder; + return folder ? true : false; +} + +export function hasFileSelected(context: RuleContext, ...args: RuleParameter[]): boolean { + const file = context.selection.file; + return file ? true : false; +} + +export function canUpdateSelectedFolder(context: RuleContext, ...args: RuleParameter[]): boolean { + const folder = context.selection.folder; + if (folder && folder.entry) { + return nodeHasPermission(folder.entry, 'update'); + } + return false; +} + +export function nodeHasPermission(node: Node, permission: string): boolean { + if (node && permission) { + const allowableOperations = node.allowableOperations || []; + return allowableOperations.includes(permission); + } + return false; +} diff --git a/src/app/extensions/rules/core.evaluators.ts b/src/app/extensions/rules/core.evaluators.ts new file mode 100644 index 0000000000..c69b7888ea --- /dev/null +++ b/src/app/extensions/rules/core.evaluators.ts @@ -0,0 +1,47 @@ +/*! + * @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 { RuleContext } from './rule-context'; +import { RuleParameter } from './rule-parameter'; + +export function every(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!args || args.length === 0) { + return false; + } + + return args + .map(arg => context.evaluators[arg.value]) + .every(evaluator => evaluator(context)); +} + +export function some(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!args || args.length === 0) { + return false; + } + + return args + .map(arg => context.evaluators[arg.value]) + .some(evaluator => evaluator(context)); +} diff --git a/src/app/extensions/rules/rule-context.ts b/src/app/extensions/rules/rule-context.ts new file mode 100644 index 0000000000..80ac871fe0 --- /dev/null +++ b/src/app/extensions/rules/rule-context.ts @@ -0,0 +1,34 @@ +/*! + * @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 { SelectionState } from '../../store/states'; +import { RuleEvaluator } from './rule.service'; +import { NavigationState } from '../../store/states/navigation.state'; + +export interface RuleContext { + selection: SelectionState; + navigation: NavigationState; + evaluators: { [key: string]: RuleEvaluator }; +} diff --git a/src/app/extensions/rules/rule-parameter.ts b/src/app/extensions/rules/rule-parameter.ts new file mode 100644 index 0000000000..7989765031 --- /dev/null +++ b/src/app/extensions/rules/rule-parameter.ts @@ -0,0 +1,29 @@ +/*! + * @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 . + */ + +export interface RuleParameter { + type: string; + value: any; +} diff --git a/src/app/extensions/rules/rule-ref.ts b/src/app/extensions/rules/rule-ref.ts new file mode 100644 index 0000000000..da8fdb7fdb --- /dev/null +++ b/src/app/extensions/rules/rule-ref.ts @@ -0,0 +1,34 @@ +/*! + * @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 { RuleParameter } from './rule-parameter'; +import { RuleEvaluator } from './rule.service'; + +export class RuleRef { + type: string; + id?: string; + parameters?: Array; + evaluator?: RuleEvaluator; +} diff --git a/src/app/extensions/rules/rule.service.ts b/src/app/extensions/rules/rule.service.ts new file mode 100644 index 0000000000..34daf60635 --- /dev/null +++ b/src/app/extensions/rules/rule.service.ts @@ -0,0 +1,97 @@ +/*! + * @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 { Injectable } from '@angular/core'; +import { AppConfigService } from '@alfresco/adf-core'; +import { every, some } from './core.evaluators'; +import { RuleContext } from './rule-context'; +import { RuleRef } from './rule-ref'; +import { createSelector, Store } from '@ngrx/store'; +import { + appSelection, + appNavigation +} from '../../store/selectors/app.selectors'; +import { AppStore, SelectionState } from '../../store/states'; +import { NavigationState } from '../../store/states/navigation.state'; +import { canCreateFolder, hasFolderSelected, canUpdateSelectedFolder, hasFileSelected, canDownloadSelection } from './app.evaluators'; + +export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean; + +export const selectionWithFolder = createSelector( + appSelection, + appNavigation, + (selection, navigation) => { + return { + selection, + navigation + }; + } +); + +@Injectable() +export class RuleService implements RuleContext { + rules: Array = []; + evaluators: { [key: string]: RuleEvaluator } = {}; + selection: SelectionState; + navigation: NavigationState; + + constructor( + private config: AppConfigService, + private store: Store + ) { + this.evaluators['core.every'] = every; + this.evaluators['core.some'] = some; + this.evaluators['app.selection.canDownload'] = canDownloadSelection; + this.evaluators['app.selection.file'] = hasFileSelected; + this.evaluators['app.selection.folder'] = hasFolderSelected; + this.evaluators['app.selection.folder.canUpdate'] = canUpdateSelectedFolder; + this.evaluators['app.navigation.folder.canCreate'] = canCreateFolder; + + this.store + .select(selectionWithFolder) + .subscribe(result => { + this.selection = result.selection; + this.navigation = result.navigation; + }); + } + + init() { + this.rules = this.config + .get>('extensions.core.rules', []) + .map(rule => { + rule.evaluator = this.evaluators[rule.type]; + return rule; + }); + } + + evaluateRule(ruleId: string): boolean { + const ruleRef = this.rules.find(ref => ref.id === ruleId); + + if (ruleRef.evaluator) { + return ruleRef.evaluator(this, ...ruleRef.parameters); + } + return false; + } +} diff --git a/src/app/store/selectors/app.selectors.ts b/src/app/store/selectors/app.selectors.ts index 49277c9d80..da23f79e10 100644 --- a/src/app/store/selectors/app.selectors.ts +++ b/src/app/store/selectors/app.selectors.ts @@ -34,4 +34,5 @@ export const appSelection = createSelector(selectApp, state => state.selection) export const appLanguagePicker = createSelector(selectApp, state => state.languagePicker); export const selectUser = createSelector(selectApp, state => state.user); export const sharedUrl = createSelector(selectApp, state => state.sharedUrl); +export const appNavigation = createSelector(selectApp, state => state.navigation); export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder); diff --git a/src/app/testing/app-testing.module.ts b/src/app/testing/app-testing.module.ts index ee3142e6d8..1376ba23ba 100644 --- a/src/app/testing/app-testing.module.ts +++ b/src/app/testing/app-testing.module.ts @@ -60,6 +60,7 @@ import { NodeActionsService } from '../common/services/node-actions.service'; import { NodePermissionService } from '../common/services/node-permission.service'; import { ContentApiService } from '../services/content-api.service'; import { ExtensionService } from '../extensions/extension.service'; +import { RuleService } from '../extensions/rules/rule.service'; @NgModule({ imports: [ @@ -112,7 +113,8 @@ import { ExtensionService } from '../extensions/extension.service'; NodeActionsService, NodePermissionService, ContentApiService, - ExtensionService + ExtensionService, + RuleService ] }) export class AppTestingModule {} From 5d8a9057bcd4f30bbe6c538b7ecc404440494f6d Mon Sep 17 00:00:00 2001 From: Suzana Dirla Date: Tue, 17 Jul 2018 08:38:36 +0300 Subject: [PATCH 030/146] [ACA-1440] use ADF Header component (#508) * [ACA-1440] use ADF HEADER component * moved style to theme * removed ACA HeaderComponent * remove aca header component theme * Update layout.component.html * update app header locator --- e2e/components/header/header.ts | 2 +- src/app/app.module.ts | 2 - .../components/header/header.component.html | 24 ----- .../header/header.component.spec.ts | 88 ------------------- .../header/header.component.theme.scss | 59 ------------- src/app/components/header/header.component.ts | 54 ------------ .../components/layout/layout.component.html | 15 +++- src/app/components/layout/layout.component.ts | 20 +++-- src/app/ui/custom-theme.scss | 4 +- .../ui/overrides/adf-layout-header.theme.scss | 21 +++++ 10 files changed, 53 insertions(+), 236 deletions(-) delete mode 100644 src/app/components/header/header.component.html delete mode 100644 src/app/components/header/header.component.spec.ts delete mode 100644 src/app/components/header/header.component.theme.scss delete mode 100644 src/app/components/header/header.component.ts create mode 100644 src/app/ui/overrides/adf-layout-header.theme.scss diff --git a/e2e/components/header/header.ts b/e2e/components/header/header.ts index c5e5f944a2..fa1c9bcdf2 100755 --- a/e2e/components/header/header.ts +++ b/e2e/components/header/header.ts @@ -37,6 +37,6 @@ export class Header extends Component { userInfo: UserInfo = new UserInfo(this.component); constructor(ancestor?: ElementFinder) { - super('aca-header', ancestor); + super('adf-layout-header', ancestor); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2bf23b8413..88140f2038 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -45,7 +45,6 @@ import { SharedFilesComponent } from './components/shared-files/shared-files.com import { TrashcanComponent } from './components/trashcan/trashcan.component'; import { LayoutComponent } from './components/layout/layout.component'; import { SidenavViewsManagerDirective } from './components/layout/sidenav-views-manager.directive'; -import { HeaderComponent } from './components/header/header.component'; import { CurrentUserComponent } from './components/current-user/current-user.component'; import { SearchInputComponent } from './components/search/search-input/search-input.component'; import { SearchInputControlComponent } from './components/search/search-input-control/search-input-control.component'; @@ -110,7 +109,6 @@ import { RuleService } from './extensions/rules/rule.service'; LoginComponent, LayoutComponent, SidenavViewsManagerDirective, - HeaderComponent, CurrentUserComponent, SearchInputComponent, SearchInputControlComponent, diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html deleted file mode 100644 index 2a4668d66f..0000000000 --- a/src/app/components/header/header.component.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - {{ appName$ | async }} - - - -
- - - - - - -
diff --git a/src/app/components/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts deleted file mode 100644 index e1ff277be5..0000000000 --- a/src/app/components/header/header.component.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -/*! - * @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 { AppConfigService } from '@alfresco/adf-core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Store } from '@ngrx/store'; -import { SetAppNameAction, SetHeaderColorAction } from '../../store/actions'; -import { AppStore } from '../../store/states/app.state'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { HeaderComponent } from './header.component'; - -describe('HeaderComponent', () => { - let fixture: ComponentFixture; - let component: HeaderComponent; - let appConfigService: AppConfigService; - let store: Store; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ AppTestingModule ], - declarations: [ - HeaderComponent - ], - schemas: [ NO_ERRORS_SCHEMA ] - }); - - store = TestBed.get(Store); - store.dispatch(new SetAppNameAction('app-name')); - store.dispatch(new SetHeaderColorAction('some-color')); - - fixture = TestBed.createComponent(HeaderComponent); - component = fixture.componentInstance; - appConfigService = TestBed.get(AppConfigService); - - spyOn(appConfigService, 'get').and.callFake((val) => { - if (val === 'application.name') { - return 'app-name'; - } - - if (val === 'headerColor') { - return 'some-color'; - } - - if (val === 'application.logo') { - return ''; - } - }); - - fixture.detectChanges(); - }); - - it('it should set application name', done => { - component.appName$.subscribe(val => { - expect(val).toBe('app-name'); - done(); - }); - }); - - it('it should set header background color', done => { - component.headerColor$.subscribe(val => { - expect(val).toBe('some-color'); - done(); - }); - }); -}); diff --git a/src/app/components/header/header.component.theme.scss b/src/app/components/header/header.component.theme.scss deleted file mode 100644 index 8b56a42247..0000000000 --- a/src/app/components/header/header.component.theme.scss +++ /dev/null @@ -1,59 +0,0 @@ -@mixin aca-header-theme($theme) { - $background: map-get($theme, background); - $app-menu-height: 64px; - - .aca-header { - - .app-menu { - height: $app-menu-height; - - &.adf-toolbar { - .mat-toolbar { - background-color: inherit; - font-family: inherit; - min-height: $app-menu-height; - height: $app-menu-height; - - .mat-toolbar-layout { - height: $app-menu-height; - - .mat-toolbar-row { - height: $app-menu-height; - } - } - } - - .adf-toolbar-divider { - margin-left: 5px; - margin-right: 5px; - - & > div { - background-color: mat-color($background, card); - } - } - - .adf-toolbar-title { - color: mat-color($background, card); - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - } - } - - .app-menu__title { - width: 100px; - height: 50px; - margin-left: 40px; - display: flex; - justify-content: center; - align-items: stretch; - - &> img { - width: 100%; - object-fit: contain; - } - } - } - } -} diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts deleted file mode 100644 index 6ad498bb3c..0000000000 --- a/src/app/components/header/header.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*! - * @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 { Component, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Rx'; -import { AppStore } from '../../store/states/app.state'; -import { selectHeaderColor, selectAppName, selectLogoPath } from '../../store/selectors/app.selectors'; - -@Component({ - selector: 'aca-header', - templateUrl: './header.component.html', - encapsulation: ViewEncapsulation.None, - host: { class: 'aca-header' } -}) -export class HeaderComponent { - @Output() menu: EventEmitter = new EventEmitter(); - - appName$: Observable; - headerColor$: Observable; - logo$: Observable; - - constructor(store: Store) { - this.headerColor$ = store.select(selectHeaderColor); - this.appName$ = store.select(selectAppName); - this.logo$ = store.select(selectLogoPath); - } - - toggleMenu() { - this.menu.emit(); - } -} diff --git a/src/app/components/layout/layout.component.html b/src/app/components/layout/layout.component.html index 6f10394a1d..81d54629d8 100644 --- a/src/app/components/layout/layout.component.html +++ b/src/app/components/layout/layout.component.html @@ -15,7 +15,20 @@ - + + +
+ + + + + + + +
diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index 62e0f49002..3681d095f4 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -23,20 +23,21 @@ * along with Alfresco. If not, see . */ -import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; -import { Subject } from 'rxjs/Rx'; +import { Component, OnInit, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Observable, Subject } from 'rxjs/Rx'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { NodePermissionService } from '../../common/services/node-permission.service'; import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states'; -import { currentFolder } from '../../store/selectors/app.selectors'; +import { currentFolder, selectAppName, selectHeaderColor, selectLogoPath } from '../../store/selectors/app.selectors'; import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-layout', templateUrl: './layout.component.html', - styleUrls: ['./layout.component.scss'] + styleUrls: ['./layout.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class LayoutComponent implements OnInit, OnDestroy { @ViewChild(SidenavViewsManagerDirective) manager: SidenavViewsManagerDirective; @@ -46,9 +47,18 @@ export class LayoutComponent implements OnInit, OnDestroy { node: MinimalNodeEntryEntity; canUpload = false; + appName$: Observable; + headerColor$: Observable; + logo$: Observable; + constructor( protected store: Store, - private permission: NodePermissionService) {} + private permission: NodePermissionService) { + + this.headerColor$ = store.select(selectHeaderColor); + this.appName$ = store.select(selectAppName); + this.logo$ = store.select(selectLogoPath); + } ngOnInit() { if (!this.manager.minimizeSidenav) { diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss index 1bb506ee84..9cc9a66b8a 100644 --- a/src/app/ui/custom-theme.scss +++ b/src/app/ui/custom-theme.scss @@ -7,7 +7,6 @@ @import '../components/search/search-input/search-input.component.theme'; @import '../components/settings/settings.component.theme'; @import '../components/current-user/current-user.component.theme'; -@import '../components/header/header.component.theme'; @import '../components/permission-manager/permissions-manager.component.theme'; @import '../dialogs/node-versions/node-versions.dialog.theme'; @@ -23,6 +22,7 @@ @import './overrides/adf-upload-drag-area.theme'; @import './overrides/adf-search-sorting-picker.theme'; @import './overrides/adf-content-node-selector.theme'; +@import './overrides/adf-layout-header.theme'; @import 'layout'; @import 'snackbar'; @@ -79,9 +79,9 @@ $custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent); @include adf-upload-drag-area-theme($theme); @include adf-search-sorting-picker-theme($theme); @include adf-content-node-selector-theme($theme); + @include adf-layout-header-theme($theme); @include aca-layout-theme($theme); - @include aca-header-theme($theme); @include aca-search-input-theme($theme); @include aca-generic-error-theme($theme); @include aca-permissions-manager-theme($theme); diff --git a/src/app/ui/overrides/adf-layout-header.theme.scss b/src/app/ui/overrides/adf-layout-header.theme.scss new file mode 100644 index 0000000000..4d13d55515 --- /dev/null +++ b/src/app/ui/overrides/adf-layout-header.theme.scss @@ -0,0 +1,21 @@ +@mixin adf-layout-header-theme($theme) { + $background: map-get($theme, background); + + .adf-layout-header { + .mat-toolbar-single-row { + .adf-app-logo { + width: 100px; + height: 50px; + margin-left: 40px; + } + } + + .adf-toolbar-divider { + margin: 0 5px; + + & > div { + background-color: mat-color($background, card, 1); + } + } + } +} From 79a20c65fb88d9bf646fbe231bc1fc2d5f8adbee Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 05:16:10 +0100 Subject: [PATCH 031/146] split extension actions into separate service (#512) * move action management to a separate ActionService * code improvements and registration chaining * code fixes --- src/app/app.module.ts | 8 +- .../action-ref.ts} | 2 +- .../extensions/actions/action.service.spec.ts | 141 ++++++++++++++++++ src/app/extensions/actions/action.service.ts | 76 ++++++++++ ...xtensions.ts => core.extensions.module.ts} | 17 ++- src/app/extensions/extension.config.ts | 8 +- src/app/extensions/extension.service.spec.ts | 96 ------------ src/app/extensions/extension.service.ts | 67 ++++----- .../{route.extension.ts => route-ref.ts} | 2 +- src/app/testing/app-testing.module.ts | 4 +- 10 files changed, 262 insertions(+), 159 deletions(-) rename src/app/extensions/{action.extension.ts => actions/action-ref.ts} (97%) create mode 100644 src/app/extensions/actions/action.service.spec.ts create mode 100644 src/app/extensions/actions/action.service.ts rename src/app/extensions/{core.extensions.ts => core.extensions.module.ts} (79%) rename src/app/extensions/{route.extension.ts => route-ref.ts} (97%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 88140f2038..79285a6317 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,13 +78,11 @@ import { MaterialModule } from './material.module'; import { ExperimentalDirective } from './directives/experimental.directive'; import { ContentApiService } from './services/content-api.service'; import { ExtensionsModule } from './extensions.module'; -import { ExtensionService } from './extensions/extension.service'; -import { CoreExtensionsModule } from './extensions/core.extensions'; +import { CoreExtensionsModule } from './extensions/core.extensions.module'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; import { NodePermissionsDirective } from './common/directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; -import { RuleService } from './extensions/rules/rule.service'; @NgModule({ imports: [ @@ -159,9 +157,7 @@ import { RuleService } from './extensions/rules/rule.service'; NodePermissionService, ProfileResolver, ExperimentalGuard, - ContentApiService, - ExtensionService, - RuleService + ContentApiService ], entryComponents: [ LibraryDialogComponent, diff --git a/src/app/extensions/action.extension.ts b/src/app/extensions/actions/action-ref.ts similarity index 97% rename from src/app/extensions/action.extension.ts rename to src/app/extensions/actions/action-ref.ts index b7fac6928b..f77d166d5b 100644 --- a/src/app/extensions/action.extension.ts +++ b/src/app/extensions/actions/action-ref.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -export interface ActionExtension { +export interface ActionRef { id: string; type: string; payload?: string; diff --git a/src/app/extensions/actions/action.service.spec.ts b/src/app/extensions/actions/action.service.spec.ts new file mode 100644 index 0000000000..75658758c1 --- /dev/null +++ b/src/app/extensions/actions/action.service.spec.ts @@ -0,0 +1,141 @@ +/*! + * @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 { AppConfigService } from '@alfresco/adf-core'; +import { ActionService } from './action.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { TestBed } from '@angular/core/testing'; +import { AppTestingModule } from '../../testing/app-testing.module'; + +describe('ActionService', () => { + let config: AppConfigService; + let actions: ActionService; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule] + }); + + actions = TestBed.get(ActionService); + store = TestBed.get(Store); + + config = TestBed.get(AppConfigService); + config.config['extensions'] = {}; + }); + + describe('actions', () => { + beforeEach(() => { + config.config.extensions = { + core: { + actions: [ + { + id: 'aca:actions/create-folder', + type: 'CREATE_FOLDER', + payload: 'folder-name' + } + ] + } + }; + }); + + it('should load actions from the config', () => { + actions.init(); + expect(actions.actions.length).toBe(1); + }); + + it('should have an empty action list if config provides nothing', () => { + config.config.extensions = {}; + actions.init(); + + expect(actions.actions).toEqual([]); + }); + + it('should find action by id', () => { + actions.init(); + + const action = actions.getActionById( + 'aca:actions/create-folder' + ); + expect(action).toBeTruthy(); + expect(action.type).toBe('CREATE_FOLDER'); + expect(action.payload).toBe('folder-name'); + }); + + it('should not find action by id', () => { + actions.init(); + + const action = actions.getActionById('missing'); + expect(action).toBeFalsy(); + }); + + it('should run the action via store', () => { + actions.init(); + spyOn(store, 'dispatch').and.stub(); + + actions.runActionById('aca:actions/create-folder'); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'CREATE_FOLDER', + payload: 'folder-name' + }); + }); + + it('should not use store if action is missing', () => { + actions.init(); + spyOn(store, 'dispatch').and.stub(); + + actions.runActionById('missing'); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + describe('expressions', () => { + it('should eval static value', () => { + const value = actions.runExpression('hello world'); + expect(value).toBe('hello world'); + }); + + it('should eval string as an expression', () => { + const value = actions.runExpression('$( "hello world" )'); + expect(value).toBe('hello world'); + }); + + it('should eval expression with no context', () => { + const value = actions.runExpression('$( 1 + 1 )'); + expect(value).toBe(2); + }); + + it('should eval expression with context', () => { + const context = { + a: 'hey', + b: 'there' + }; + const expression = '$( context.a + " " + context.b + "!" )'; + const value = actions.runExpression(expression, context); + expect(value).toBe('hey there!'); + }); + }); +}); diff --git a/src/app/extensions/actions/action.service.ts b/src/app/extensions/actions/action.service.ts new file mode 100644 index 0000000000..c0245049f8 --- /dev/null +++ b/src/app/extensions/actions/action.service.ts @@ -0,0 +1,76 @@ +/*! + * @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 { Injectable } from '@angular/core'; +import { AppConfigService } from '@alfresco/adf-core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { ActionRef } from './action-ref'; + +@Injectable() +export class ActionService { + actions: Array = []; + + constructor( + private config: AppConfigService, + private store: Store + ) {} + + init() { + this.actions = this.config.get>( + 'extensions.core.actions', + [] + ); + } + + getActionById(id: string): ActionRef { + return this.actions.find(action => action.id === id); + } + + runActionById(id: string, context?: any) { + const action = this.getActionById(id); + if (action) { + const { type, payload } = action; + const expression = this.runExpression(payload, context); + + this.store.dispatch({ type, payload: expression }); + } + } + + runExpression(value: string, context?: any) { + const pattern = new RegExp(/\$\((.*\)?)\)/g); + const matches = pattern.exec(value); + + if (matches && matches.length > 1) { + const expression = matches[1]; + const fn = new Function('context', `return ${expression}`); + const result = fn(context); + + return result; + } + + return value; + } +} diff --git a/src/app/extensions/core.extensions.ts b/src/app/extensions/core.extensions.module.ts similarity index 79% rename from src/app/extensions/core.extensions.ts rename to src/app/extensions/core.extensions.module.ts index e58bfc6cbb..488175068e 100644 --- a/src/app/extensions/core.extensions.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -30,20 +30,21 @@ import { AboutComponent } from '../components/about/about.component'; import { LayoutComponent } from '../components/layout/layout.component'; import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component'; import { CommonModule } from '@angular/common'; +import { RuleService } from './rules/rule.service'; +import { ActionService } from './actions/action.service'; @NgModule({ - imports: [ - CommonModule, - CoreModule.forChild() - ], + imports: [CommonModule, CoreModule.forChild()], declarations: [ToolbarActionComponent], exports: [ToolbarActionComponent], - entryComponents: [AboutComponent] + entryComponents: [AboutComponent], + providers: [ExtensionService, RuleService, ActionService] }) export class CoreExtensionsModule { constructor(extensions: ExtensionService) { - extensions.components['aca:layouts/main'] = LayoutComponent; - extensions.components['aca:components/about'] = AboutComponent; - extensions.authGuards['aca:auth'] = AuthGuardEcm; + extensions + .setComponent('aca:layouts/main', LayoutComponent) + .setComponent('aca:components/about', AboutComponent) + .setAuthGuard('aca:auth', AuthGuardEcm); } } diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index 0a5e9af205..d239c47af8 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -23,13 +23,13 @@ * along with Alfresco. If not, see . */ -import { RouteExtension } from './route.extension'; -import { ActionExtension } from './action.extension'; import { RuleRef } from './rules/rule-ref'; +import { ActionRef } from './actions/action-ref'; +import { RouteRef } from './route-ref'; export interface ExtensionConfig { rules?: Array; - routes?: Array; - actions?: Array; + routes?: Array; + actions?: Array; features?: { [key: string]: any }; } diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts index fa1e27278d..ac0af9e5cb 100644 --- a/src/app/extensions/extension.service.spec.ts +++ b/src/app/extensions/extension.service.spec.ts @@ -27,14 +27,11 @@ import { TestBed } from '@angular/core/testing'; import { AppTestingModule } from '../testing/app-testing.module'; import { ExtensionService } from './extension.service'; import { AppConfigService } from '@alfresco/adf-core'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states'; import { ContentActionType } from './content-action.extension'; describe('ExtensionService', () => { let config: AppConfigService; let extensions: ExtensionService; - let store: Store; beforeEach(() => { TestBed.configureTestingModule({ @@ -42,7 +39,6 @@ describe('ExtensionService', () => { }); extensions = TestBed.get(ExtensionService); - store = TestBed.get(Store); config = TestBed.get(AppConfigService); config.config['extensions'] = {}; @@ -168,71 +164,6 @@ describe('ExtensionService', () => { }); }); - describe('actions', () => { - beforeEach(() => { - config.config.extensions = { - core: { - actions: [ - { - id: 'aca:actions/create-folder', - type: 'CREATE_FOLDER', - payload: 'folder-name' - } - ] - } - }; - }); - - it('should load actions from the config', () => { - extensions.init(); - expect(extensions.actions.length).toBe(1); - }); - - it('should have an empty action list if config provides nothing', () => { - config.config.extensions = {}; - extensions.init(); - - expect(extensions.actions).toEqual([]); - }); - - it('should find action by id', () => { - extensions.init(); - - const action = extensions.getActionById( - 'aca:actions/create-folder' - ); - expect(action).toBeTruthy(); - expect(action.type).toBe('CREATE_FOLDER'); - expect(action.payload).toBe('folder-name'); - }); - - it('should not find action by id', () => { - extensions.init(); - - const action = extensions.getActionById('missing'); - expect(action).toBeFalsy(); - }); - - it('should run the action via store', () => { - extensions.init(); - spyOn(store, 'dispatch').and.stub(); - - extensions.runActionById('aca:actions/create-folder'); - expect(store.dispatch).toHaveBeenCalledWith({ - type: 'CREATE_FOLDER', - payload: 'folder-name' - }); - }); - - it('should not use store if action is missing', () => { - extensions.init(); - spyOn(store, 'dispatch').and.stub(); - - extensions.runActionById('missing'); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - describe('content actions', () => { it('should load content actions from the config', () => { config.config.extensions = { @@ -464,33 +395,6 @@ describe('ExtensionService', () => { }); }); - describe('expressions', () => { - it('should eval static value', () => { - const value = extensions.runExpression('hello world'); - expect(value).toBe('hello world'); - }); - - it('should eval string as an expression', () => { - const value = extensions.runExpression('$( "hello world" )'); - expect(value).toBe('hello world'); - }); - - it('should eval expression with no context', () => { - const value = extensions.runExpression('$( 1 + 1 )'); - expect(value).toBe(2); - }); - - it('should eval expression with context', () => { - const context = { - a: 'hey', - b: 'there' - }; - const expression = '$( context.a + " " + context.b + "!" )'; - const value = extensions.runExpression(expression, context); - expect(value).toBe('hey there!'); - }); - }); - describe('sorting', () => { it('should sort by provided order', () => { const sorted = [ diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 6991c81fa4..3d514ee151 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -24,52 +24,45 @@ */ import { Injectable, Type } from '@angular/core'; -import { RouteExtension } from './route.extension'; -import { ActionExtension } from './action.extension'; import { AppConfigService } from '@alfresco/adf-core'; import { ContentActionExtension, ContentActionType } from './content-action.extension'; import { OpenWithExtension } from './open-with.extension'; -import { AppStore } from '../store/states'; -import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; import { Node } from 'alfresco-js-api'; import { RuleService } from './rules/rule.service'; +import { ActionService } from './actions/action.service'; +import { ActionRef } from './actions/action-ref'; +import { RouteRef } from './route-ref'; @Injectable() export class ExtensionService { - routes: Array = []; - actions: Array = []; contentActions: Array = []; openWithActions: Array = []; createActions: Array = []; + routes: Array = []; authGuards: { [key: string]: Type<{}> } = {}; components: { [key: string]: Type<{}> } = {}; constructor( private config: AppConfigService, - private store: Store, - private ruleService: RuleService + private ruleService: RuleService, + private actionService: ActionService ) {} // initialise extension service // in future will also load and merge data from the external plugins init() { - this.routes = this.config.get>( + this.routes = this.config.get>( 'extensions.core.routes', [] ); - this.actions = this.config.get>( - 'extensions.core.actions', - [] - ); - this.contentActions = this.config .get>( 'extensions.core.features.content.actions', @@ -93,24 +86,30 @@ export class ExtensionService { .sort(this.sortByOrder); this.ruleService.init(); + this.actionService.init(); } - getRouteById(id: string): RouteExtension { + setAuthGuard(key: string, value: Type<{}>): ExtensionService { + this.authGuards[key] = value; + return this; + } + + getRouteById(id: string): RouteRef { return this.routes.find(route => route.id === id); } - getActionById(id: string): ActionExtension { - return this.actions.find(action => action.id === id); + getActionById(id: string): ActionRef { + return this.actionService.getActionById(id); } runActionById(id: string, context?: any) { - const action = this.getActionById(id); - if (action) { - const { type, payload } = action; - const expression = this.runExpression(payload, context); + this.actionService.runActionById(id, context); + } - this.store.dispatch({ type, payload: expression }); - } + getAuthGuards(ids: string[]): Array> { + return (ids || []) + .map(id => this.authGuards[id]) + .filter(guard => guard); } getNavigationGroups(): Array { @@ -140,31 +139,15 @@ export class ExtensionService { return []; } - getAuthGuards(ids: string[]): Array> { - return (ids || []) - .map(id => this.authGuards[id]) - .filter(guard => guard); + setComponent(id: string, value: Type<{}>): ExtensionService { + this.components[id] = value; + return this; } getComponentById(id: string): Type<{}> { return this.components[id]; } - runExpression(value: string, context?: any) { - const pattern = new RegExp(/\$\((.*\)?)\)/g); - const matches = pattern.exec(value); - - if (matches && matches.length > 1) { - const expression = matches[1]; - const fn = new Function('context', `return ${expression}`); - const result = fn(context); - - return result; - } - - return value; - } - getApplicationRoutes(): Array { return this.routes.map(route => { const guards = this.getAuthGuards(route.auth); diff --git a/src/app/extensions/route.extension.ts b/src/app/extensions/route-ref.ts similarity index 97% rename from src/app/extensions/route.extension.ts rename to src/app/extensions/route-ref.ts index 8f2cb99599..2a0d5992c7 100644 --- a/src/app/extensions/route.extension.ts +++ b/src/app/extensions/route-ref.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -export interface RouteExtension { +export interface RouteRef { id: string; path: string; component: string; diff --git a/src/app/testing/app-testing.module.ts b/src/app/testing/app-testing.module.ts index 1376ba23ba..43d40f3adf 100644 --- a/src/app/testing/app-testing.module.ts +++ b/src/app/testing/app-testing.module.ts @@ -61,6 +61,7 @@ import { NodePermissionService } from '../common/services/node-permission.servic import { ContentApiService } from '../services/content-api.service'; import { ExtensionService } from '../extensions/extension.service'; import { RuleService } from '../extensions/rules/rule.service'; +import { ActionService } from '../extensions/actions/action.service'; @NgModule({ imports: [ @@ -114,7 +115,8 @@ import { RuleService } from '../extensions/rules/rule.service'; NodePermissionService, ContentApiService, ExtensionService, - RuleService + RuleService, + ActionService ] }) export class AppTestingModule {} From 4d7b92823e484b62d5b63f746dc288c1bdea0f95 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 09:43:57 +0100 Subject: [PATCH 032/146] deprecate "common" folder, use standard structure (#514) --- src/app/app.module.ts | 26 +++++++++---------- src/app/app.routes.ts | 4 +-- .../favorites/favorites.component.spec.ts | 2 +- .../favorites/favorites.component.ts | 2 +- .../components/files/files.component.spec.ts | 4 +-- src/app/components/files/files.component.ts | 4 +-- .../info-drawer/info-drawer.component.ts | 2 +- src/app/components/layout/layout.component.ts | 2 +- .../libraries/libraries.component.ts | 2 +- src/app/components/page.component.ts | 2 +- .../components/preview/preview.component.ts | 2 +- .../recent-files.component.spec.ts | 2 +- .../recent-files/recent-files.component.ts | 2 +- .../search-results.component.ts | 2 +- .../shared-files.component.spec.ts | 2 +- .../shared-files/shared-files.component.ts | 2 +- .../components/sidenav/sidenav.component.ts | 2 +- .../trashcan/trashcan.component.spec.ts | 2 +- .../components/trashcan/trashcan.component.ts | 2 +- .../directives/node-copy.directive.spec.ts | 4 +-- .../directives/node-copy.directive.ts | 2 +- .../directives/node-delete.directive.spec.ts | 8 +++--- .../directives/node-delete.directive.ts | 6 ++--- .../directives/node-move.directive.spec.ts | 8 +++--- .../directives/node-move.directive.ts | 6 ++--- .../node-permanent-delete.directive.spec.ts | 8 +++--- .../node-permanent-delete.directive.ts | 6 ++--- .../directives/node-permissions.directive.ts | 6 ++--- .../directives/node-restore.directive.spec.ts | 6 ++--- .../directives/node-restore.directive.ts | 8 +++--- .../directives/node-unshare.directive.ts | 2 +- .../directives/node-versions.directive.ts | 8 +++--- .../services/content-management.service.ts | 6 ++--- .../services/experimental-guard.service.ts | 2 +- .../services/node-actions.service.spec.ts | 4 +-- .../services/node-actions.service.ts | 2 +- .../services/node-permission.service.spec.ts | 0 .../services/node-permission.service.ts | 0 .../{common => }/services/profile.resolver.ts | 6 ++--- src/app/store/effects/library.effects.ts | 2 +- src/app/store/effects/node.effects.ts | 2 +- src/app/testing/app-testing.module.ts | 6 ++--- 42 files changed, 88 insertions(+), 88 deletions(-) rename src/app/{common => }/directives/node-copy.directive.spec.ts (98%) rename src/app/{common => }/directives/node-copy.directive.ts (98%) rename src/app/{common => }/directives/node-delete.directive.spec.ts (97%) rename src/app/{common => }/directives/node-delete.directive.ts (92%) rename src/app/{common => }/directives/node-move.directive.spec.ts (98%) rename src/app/{common => }/directives/node-move.directive.ts (97%) rename src/app/{common => }/directives/node-permanent-delete.directive.spec.ts (97%) rename src/app/{common => }/directives/node-permanent-delete.directive.ts (94%) rename src/app/{common => }/directives/node-permissions.directive.ts (91%) rename src/app/{common => }/directives/node-restore.directive.spec.ts (98%) rename src/app/{common => }/directives/node-restore.directive.ts (97%) rename src/app/{common => }/directives/node-unshare.directive.ts (96%) rename src/app/{common => }/directives/node-versions.directive.ts (90%) rename src/app/{common => }/services/content-management.service.ts (96%) rename src/app/{common => }/services/experimental-guard.service.ts (93%) rename src/app/{common => }/services/node-actions.service.spec.ts (99%) rename src/app/{common => }/services/node-actions.service.ts (99%) rename src/app/{common => }/services/node-permission.service.spec.ts (100%) rename src/app/{common => }/services/node-permission.service.ts (100%) rename src/app/{common => }/services/profile.resolver.ts (91%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 79285a6317..6c53b95a4b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -52,22 +52,22 @@ import { SidenavComponent } from './components/sidenav/sidenav.component'; import { AboutComponent } from './components/about/about.component'; import { LocationLinkComponent } from './components/location-link/location-link.component'; import { SharedLinkViewComponent } from './components/shared-link-view/shared-link-view.component'; -import { NodeCopyDirective } from './common/directives/node-copy.directive'; -import { NodeDeleteDirective } from './common/directives/node-delete.directive'; -import { NodeMoveDirective } from './common/directives/node-move.directive'; -import { NodeRestoreDirective } from './common/directives/node-restore.directive'; -import { NodePermanentDeleteDirective } from './common/directives/node-permanent-delete.directive'; -import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; -import { NodeVersionsDirective } from './common/directives/node-versions.directive'; +import { NodeCopyDirective } from './directives/node-copy.directive'; +import { NodeDeleteDirective } from './directives/node-delete.directive'; +import { NodeMoveDirective } from './directives/node-move.directive'; +import { NodeRestoreDirective } from './directives/node-restore.directive'; +import { NodePermanentDeleteDirective } from './directives/node-permanent-delete.directive'; +import { NodeUnshareDirective } from './directives/node-unshare.directive'; +import { NodeVersionsDirective } from './directives/node-versions.directive'; import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog'; import { LibraryDialogComponent } from './dialogs/library/library.dialog'; -import { ContentManagementService } from './common/services/content-management.service'; -import { NodeActionsService } from './common/services/node-actions.service'; -import { NodePermissionService } from './common/services/node-permission.service'; +import { ContentManagementService } from './services/content-management.service'; +import { NodeActionsService } from './services/node-actions.service'; +import { NodePermissionService } from './services/node-permission.service'; import { SearchResultsComponent } from './components/search/search-results/search-results.component'; import { SettingsComponent } from './components/settings/settings.component'; -import { ProfileResolver } from './common/services/profile.resolver'; -import { ExperimentalGuard } from './common/services/experimental-guard.service'; +import { ProfileResolver } from './services/profile.resolver'; +import { ExperimentalGuard } from './services/experimental-guard.service'; import { InfoDrawerComponent } from './components/info-drawer/info-drawer.component'; import { EditFolderDirective } from './directives/edit-folder.directive'; @@ -81,7 +81,7 @@ import { ExtensionsModule } from './extensions.module'; import { CoreExtensionsModule } from './extensions/core.extensions.module'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; -import { NodePermissionsDirective } from './common/directives/node-permissions.directive'; +import { NodePermissionsDirective } from './directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; @NgModule({ diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 8a4a42a698..c7725a5bbd 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -43,8 +43,8 @@ import { GenericErrorComponent } from './components/generic-error/generic-error. import { SearchResultsComponent } from './components/search/search-results/search-results.component'; import { SettingsComponent } from './components/settings/settings.component'; -import { ProfileResolver } from './common/services/profile.resolver'; -import { ExperimentalGuard } from './common/services/experimental-guard.service'; +import { ProfileResolver } from './services/profile.resolver'; +import { ExperimentalGuard } from './services/experimental-guard.service'; export const APP_ROUTES: Routes = [ { diff --git a/src/app/components/favorites/favorites.component.spec.ts b/src/app/components/favorites/favorites.component.spec.ts index 67076d2cd2..77f5b02d29 100644 --- a/src/app/components/favorites/favorites.component.spec.ts +++ b/src/app/components/favorites/favorites.component.spec.ts @@ -33,7 +33,7 @@ import { NodeFavoriteDirective, DataTableComponent, AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { FavoritesComponent } from './favorites.component'; import { AppTestingModule } from '../../testing/app-testing.module'; diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts index 3631341431..da332aa3fa 100644 --- a/src/app/components/favorites/favorites.component.ts +++ b/src/app/components/favorites/favorites.component.ts @@ -32,7 +32,7 @@ import { PathElementEntity, PathInfo } from 'alfresco-js-api'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { AppStore } from '../../store/states/app.state'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts index 6783216917..a8f706818a 100644 --- a/src/app/components/files/files.component.spec.ts +++ b/src/app/components/files/files.component.spec.ts @@ -32,8 +32,8 @@ import { DataTableComponent, UploadService, AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; -import { ContentManagementService } from '../../common/services/content-management.service'; -import { NodeActionsService } from '../../common/services/node-actions.service'; +import { ContentManagementService } from '../../services/content-management.service'; +import { NodeActionsService } from '../../services/node-actions.service'; import { FilesComponent } from './files.component'; import { AppTestingModule } from '../../testing/app-testing.module'; import { ContentApiService } from '../../services/content-api.service'; diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index 4e98f104b9..22efb389a3 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -29,8 +29,8 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Rx'; -import { ContentManagementService } from '../../common/services/content-management.service'; -import { NodeActionsService } from '../../common/services/node-actions.service'; +import { ContentManagementService } from '../../services/content-management.service'; +import { NodeActionsService } from '../../services/node-actions.service'; import { AppStore } from '../../store/states/app.state'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; diff --git a/src/app/components/info-drawer/info-drawer.component.ts b/src/app/components/info-drawer/info-drawer.component.ts index 116e1ca933..4f50919e79 100644 --- a/src/app/components/info-drawer/info-drawer.component.ts +++ b/src/app/components/info-drawer/info-drawer.component.ts @@ -25,7 +25,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { NodePermissionService } from '../../common/services/node-permission.service'; +import { NodePermissionService } from '../../services/node-permission.service'; import { ContentApiService } from '../../services/content-api.service'; @Component({ diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index 3681d095f4..6ee3e68ec2 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -26,7 +26,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; import { Observable, Subject } from 'rxjs/Rx'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { NodePermissionService } from '../../common/services/node-permission.service'; +import { NodePermissionService } from '../../services/node-permission.service'; import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states'; diff --git a/src/app/components/libraries/libraries.component.ts b/src/app/components/libraries/libraries.component.ts index 0aec560108..841b1e3933 100644 --- a/src/app/components/libraries/libraries.component.ts +++ b/src/app/components/libraries/libraries.component.ts @@ -32,7 +32,7 @@ import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; import { DeleteLibraryAction, CreateLibraryAction } from '../../store/actions'; import { SiteEntry } from 'alfresco-js-api'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index 95c3dd9098..c70c169d68 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -37,7 +37,7 @@ import { SelectionState } from '../store/states/selection.state'; import { Observable } from 'rxjs/Rx'; import { ExtensionService } from '../extensions/extension.service'; import { ContentActionExtension } from '../extensions/content-action.extension'; -import { ContentManagementService } from '../common/services/content-management.service'; +import { ContentManagementService } from '../services/content-management.service'; export abstract class PageComponent implements OnInit, OnDestroy { diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index 6d0816f10f..eb30884765 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -33,7 +33,7 @@ import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; import { OpenWithExtension } from '../../extensions/open-with.extension'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; @Component({ selector: 'app-preview', templateUrl: 'preview.component.html', diff --git a/src/app/components/recent-files/recent-files.component.spec.ts b/src/app/components/recent-files/recent-files.component.spec.ts index 7313beb403..b359aea591 100644 --- a/src/app/components/recent-files/recent-files.component.spec.ts +++ b/src/app/components/recent-files/recent-files.component.spec.ts @@ -30,7 +30,7 @@ import { TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent, AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { RecentFilesComponent } from './recent-files.component'; import { AppTestingModule } from '../../testing/app-testing.module'; diff --git a/src/app/components/recent-files/recent-files.component.ts b/src/app/components/recent-files/recent-files.component.ts index b4b74dfb3d..f1c6af04ea 100644 --- a/src/app/components/recent-files/recent-files.component.ts +++ b/src/app/components/recent-files/recent-files.component.ts @@ -25,7 +25,7 @@ import { Component, OnInit } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { PageComponent } from '../page.component'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; diff --git a/src/app/components/search/search-results/search-results.component.ts b/src/app/components/search/search-results/search-results.component.ts index 7e30443215..af47617e9c 100644 --- a/src/app/components/search/search-results/search-results.component.ts +++ b/src/app/components/search/search-results/search-results.component.ts @@ -32,7 +32,7 @@ import { Store } from '@ngrx/store'; import { AppStore } from '../../../store/states/app.state'; import { NavigateToFolder } from '../../../store/actions'; import { ExtensionService } from '../../../extensions/extension.service'; -import { ContentManagementService } from '../../../common/services/content-management.service'; +import { ContentManagementService } from '../../../services/content-management.service'; @Component({ selector: 'aca-search-results', diff --git a/src/app/components/shared-files/shared-files.component.spec.ts b/src/app/components/shared-files/shared-files.component.spec.ts index 690d95db75..d59f6448fd 100644 --- a/src/app/components/shared-files/shared-files.component.spec.ts +++ b/src/app/components/shared-files/shared-files.component.spec.ts @@ -30,7 +30,7 @@ import { TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent, AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { SharedFilesComponent } from './shared-files.component'; import { AppTestingModule } from '../../testing/app-testing.module'; import { ExperimentalDirective } from '../../directives/experimental.directive'; diff --git a/src/app/components/shared-files/shared-files.component.ts b/src/app/components/shared-files/shared-files.component.ts index 737fdad9a4..1479dccc54 100644 --- a/src/app/components/shared-files/shared-files.component.ts +++ b/src/app/components/shared-files/shared-files.component.ts @@ -24,7 +24,7 @@ */ import { Component, OnInit } from '@angular/core'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { PageComponent } from '../page.component'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts index 9ecf401c00..fea5fbdc41 100644 --- a/src/app/components/sidenav/sidenav.component.ts +++ b/src/app/components/sidenav/sidenav.component.ts @@ -26,7 +26,7 @@ import { Subject } from 'rxjs/Rx'; import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { Node } from 'alfresco-js-api'; -import { NodePermissionService } from '../../common/services/node-permission.service'; +import { NodePermissionService } from '../../services/node-permission.service'; import { ExtensionService } from '../../extensions/extension.service'; import { NavigationExtension } from '../../extensions/navigation.extension'; import { Store } from '@ngrx/store'; diff --git a/src/app/components/trashcan/trashcan.component.spec.ts b/src/app/components/trashcan/trashcan.component.spec.ts index e7e16dd703..c39dd0e150 100644 --- a/src/app/components/trashcan/trashcan.component.spec.ts +++ b/src/app/components/trashcan/trashcan.component.spec.ts @@ -30,7 +30,7 @@ import { NodeFavoriteDirective, DataTableComponent, AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { TrashcanComponent } from './trashcan.component'; import { AppTestingModule } from '../../testing/app-testing.module'; import { ExperimentalDirective } from '../../directives/experimental.directive'; diff --git a/src/app/components/trashcan/trashcan.component.ts b/src/app/components/trashcan/trashcan.component.ts index 5e80f61a94..fddc50518e 100644 --- a/src/app/components/trashcan/trashcan.component.ts +++ b/src/app/components/trashcan/trashcan.component.ts @@ -24,7 +24,7 @@ */ import { Component, OnInit } from '@angular/core'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { PageComponent } from '../page.component'; import { Store } from '@ngrx/store'; import { selectUser } from '../../store/selectors/app.selectors'; diff --git a/src/app/common/directives/node-copy.directive.spec.ts b/src/app/directives/node-copy.directive.spec.ts similarity index 98% rename from src/app/common/directives/node-copy.directive.spec.ts rename to src/app/directives/node-copy.directive.spec.ts index c95b985469..911c195ce2 100644 --- a/src/app/common/directives/node-copy.directive.spec.ts +++ b/src/app/directives/node-copy.directive.spec.ts @@ -30,8 +30,8 @@ import { Observable } from 'rxjs/Rx'; import { MatSnackBar } from '@angular/material'; import { NodeActionsService } from '../services/node-actions.service'; import { NodeCopyDirective } from './node-copy.directive'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { ContentApiService } from '../services/content-api.service'; +import { AppTestingModule } from '../testing/app-testing.module'; @Component({ template: '
' diff --git a/src/app/common/directives/node-copy.directive.ts b/src/app/directives/node-copy.directive.ts similarity index 98% rename from src/app/common/directives/node-copy.directive.ts rename to src/app/directives/node-copy.directive.ts index c483da02dc..c8e994190c 100644 --- a/src/app/common/directives/node-copy.directive.ts +++ b/src/app/directives/node-copy.directive.ts @@ -31,7 +31,7 @@ import { TranslationService } from '@alfresco/adf-core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { NodeActionsService } from '../services/node-actions.service'; import { ContentManagementService } from '../services/content-management.service'; -import { ContentApiService } from '../../services/content-api.service'; +import { ContentApiService } from '../services/content-api.service'; @Directive({ selector: '[acaCopyNode]' diff --git a/src/app/common/directives/node-delete.directive.spec.ts b/src/app/directives/node-delete.directive.spec.ts similarity index 97% rename from src/app/common/directives/node-delete.directive.spec.ts rename to src/app/directives/node-delete.directive.spec.ts index a9087766b6..8fe99c3d8d 100644 --- a/src/app/common/directives/node-delete.directive.spec.ts +++ b/src/app/directives/node-delete.directive.spec.ts @@ -29,14 +29,14 @@ import { Component, DebugElement } from '@angular/core'; import { NodeDeleteDirective } from './node-delete.directive'; import { EffectsModule, Actions, ofType } from '@ngrx/effects'; -import { NodeEffects } from '../../store/effects/node.effects'; +import { NodeEffects } from '../store/effects/node.effects'; import { SnackbarInfoAction, SNACKBAR_INFO, SNACKBAR_ERROR, SnackbarErrorAction, SnackbarWarningAction, SNACKBAR_WARNING -} from '../../store/actions'; +} from '../store/actions'; import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; import { Observable } from 'rxjs/Rx'; @Component({ diff --git a/src/app/common/directives/node-delete.directive.ts b/src/app/directives/node-delete.directive.ts similarity index 92% rename from src/app/common/directives/node-delete.directive.ts rename to src/app/directives/node-delete.directive.ts index c66cc4562a..a5a70a61c2 100644 --- a/src/app/common/directives/node-delete.directive.ts +++ b/src/app/directives/node-delete.directive.ts @@ -26,9 +26,9 @@ import { Directive, HostListener, Input } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; -import { DeleteNodesAction } from '../../store/actions'; -import { NodeInfo } from '../../store/models'; +import { AppStore } from '../store/states/app.state'; +import { DeleteNodesAction } from '../store/actions'; +import { NodeInfo } from '../store/models'; @Directive({ selector: '[acaDeleteNode]' diff --git a/src/app/common/directives/node-move.directive.spec.ts b/src/app/directives/node-move.directive.spec.ts similarity index 98% rename from src/app/common/directives/node-move.directive.spec.ts rename to src/app/directives/node-move.directive.spec.ts index e23209b5a8..9d0ae80028 100644 --- a/src/app/common/directives/node-move.directive.spec.ts +++ b/src/app/directives/node-move.directive.spec.ts @@ -32,11 +32,11 @@ import { TranslationService } from '@alfresco/adf-core'; import { NodeActionsService } from '../services/node-actions.service'; import { NodeMoveDirective } from './node-move.directive'; import { EffectsModule, Actions, ofType } from '@ngrx/effects'; -import { NodeEffects } from '../../store/effects/node.effects'; -import { SnackbarErrorAction, SNACKBAR_ERROR } from '../../store/actions'; +import { NodeEffects } from '../store/effects/node.effects'; +import { SnackbarErrorAction, SNACKBAR_ERROR } from '../store/actions'; import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; @Component({ template: '
' diff --git a/src/app/common/directives/node-move.directive.ts b/src/app/directives/node-move.directive.ts similarity index 97% rename from src/app/common/directives/node-move.directive.ts rename to src/app/directives/node-move.directive.ts index a1b645a47e..09aa9105e2 100644 --- a/src/app/common/directives/node-move.directive.ts +++ b/src/app/directives/node-move.directive.ts @@ -33,9 +33,9 @@ import { ContentManagementService } from '../services/content-management.service import { NodeActionsService } from '../services/node-actions.service'; import { Observable } from 'rxjs/Rx'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; -import { SnackbarErrorAction } from '../../store/actions'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppStore } from '../store/states/app.state'; +import { SnackbarErrorAction } from '../store/actions'; +import { ContentApiService } from '../services/content-api.service'; @Directive({ selector: '[acaMoveNode]' diff --git a/src/app/common/directives/node-permanent-delete.directive.spec.ts b/src/app/directives/node-permanent-delete.directive.spec.ts similarity index 97% rename from src/app/common/directives/node-permanent-delete.directive.spec.ts rename to src/app/directives/node-permanent-delete.directive.spec.ts index efde1230f3..6981144eb8 100644 --- a/src/app/common/directives/node-permanent-delete.directive.spec.ts +++ b/src/app/directives/node-permanent-delete.directive.spec.ts @@ -34,11 +34,11 @@ import { Actions, ofType, EffectsModule } from '@ngrx/effects'; import { SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction, SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING -} from '../../store/actions'; +} from '../store/actions'; import { map } from 'rxjs/operators'; -import { NodeEffects } from '../../store/effects/node.effects'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { NodeEffects } from '../store/effects/node.effects'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; @Component({ template: `
` diff --git a/src/app/common/directives/node-permanent-delete.directive.ts b/src/app/directives/node-permanent-delete.directive.ts similarity index 94% rename from src/app/common/directives/node-permanent-delete.directive.ts rename to src/app/directives/node-permanent-delete.directive.ts index 90f1e9f405..459f54360c 100644 --- a/src/app/common/directives/node-permanent-delete.directive.ts +++ b/src/app/directives/node-permanent-delete.directive.ts @@ -29,9 +29,9 @@ import { MatDialog } from '@angular/material'; import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; -import { PurgeDeletedNodesAction } from '../../store/actions'; -import { NodeInfo } from '../../store/models'; +import { AppStore } from '../store/states/app.state'; +import { PurgeDeletedNodesAction } from '../store/actions'; +import { NodeInfo } from '../store/models'; @Directive({ selector: '[acaPermanentDelete]' diff --git a/src/app/common/directives/node-permissions.directive.ts b/src/app/directives/node-permissions.directive.ts similarity index 91% rename from src/app/common/directives/node-permissions.directive.ts rename to src/app/directives/node-permissions.directive.ts index 6fe39cbe74..92973e1043 100644 --- a/src/app/common/directives/node-permissions.directive.ts +++ b/src/app/directives/node-permissions.directive.ts @@ -27,9 +27,9 @@ import { Directive, HostListener, Input } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { MatDialog } from '@angular/material'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; -import { SnackbarErrorAction } from '../../store/actions'; -import { NodePermissionsDialogComponent } from '../../dialogs/node-permissions/node-permissions.dialog'; +import { AppStore } from '../store/states/app.state'; +import { SnackbarErrorAction } from '../store/actions'; +import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog'; @Directive({ selector: '[acaNodePermissions]' diff --git a/src/app/common/directives/node-restore.directive.spec.ts b/src/app/directives/node-restore.directive.spec.ts similarity index 98% rename from src/app/common/directives/node-restore.directive.spec.ts rename to src/app/directives/node-restore.directive.spec.ts index 620c22ec9c..2380f8fb7e 100644 --- a/src/app/common/directives/node-restore.directive.spec.ts +++ b/src/app/directives/node-restore.directive.spec.ts @@ -31,10 +31,10 @@ import { ContentManagementService } from '../services/content-management.service import { Actions, ofType } from '@ngrx/effects'; import { SnackbarErrorAction, SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO, - NavigateRouteAction, NAVIGATE_ROUTE } from '../../store/actions'; + NavigateRouteAction, NAVIGATE_ROUTE } from '../store/actions'; import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; import { Observable } from 'rxjs/Rx'; @Component({ diff --git a/src/app/common/directives/node-restore.directive.ts b/src/app/directives/node-restore.directive.ts similarity index 97% rename from src/app/common/directives/node-restore.directive.ts rename to src/app/directives/node-restore.directive.ts index ddacb0b284..2179209000 100644 --- a/src/app/common/directives/node-restore.directive.ts +++ b/src/app/directives/node-restore.directive.ts @@ -31,18 +31,18 @@ import { PathInfoEntity, DeletedNodesPaging } from 'alfresco-js-api'; -import { DeleteStatus, DeletedNodeInfo } from '../../store/models'; +import { DeleteStatus, DeletedNodeInfo } from '../store/models'; import { ContentManagementService } from '../services/content-management.service'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; +import { AppStore } from '../store/states/app.state'; import { NavigateRouteAction, SnackbarAction, SnackbarErrorAction, SnackbarInfoAction, SnackbarUserAction -} from '../../store/actions'; -import { ContentApiService } from '../../services/content-api.service'; +} from '../store/actions'; +import { ContentApiService } from '../services/content-api.service'; @Directive({ selector: '[acaRestoreNode]' diff --git a/src/app/common/directives/node-unshare.directive.ts b/src/app/directives/node-unshare.directive.ts similarity index 96% rename from src/app/common/directives/node-unshare.directive.ts rename to src/app/directives/node-unshare.directive.ts index 19fdedfa74..2426e393ee 100644 --- a/src/app/common/directives/node-unshare.directive.ts +++ b/src/app/directives/node-unshare.directive.ts @@ -26,7 +26,7 @@ import { Directive, HostListener, Input } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { ContentManagementService } from '../services/content-management.service'; -import { ContentApiService } from '../../services/content-api.service'; +import { ContentApiService } from '../services/content-api.service'; @Directive({ selector: '[acaUnshareNode]' diff --git a/src/app/common/directives/node-versions.directive.ts b/src/app/directives/node-versions.directive.ts similarity index 90% rename from src/app/common/directives/node-versions.directive.ts rename to src/app/directives/node-versions.directive.ts index 55c862ef23..aceddc086d 100644 --- a/src/app/common/directives/node-versions.directive.ts +++ b/src/app/directives/node-versions.directive.ts @@ -25,12 +25,12 @@ import { Directive, HostListener, Input } from '@angular/core'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { NodeVersionsDialogComponent } from '../../dialogs/node-versions/node-versions.dialog'; +import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog'; import { MatDialog } from '@angular/material'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states/app.state'; -import { SnackbarErrorAction } from '../../store/actions'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppStore } from '../store/states/app.state'; +import { SnackbarErrorAction } from '../store/actions'; +import { ContentApiService } from '../services/content-api.service'; @Directive({ selector: '[acaNodeVersions]' diff --git a/src/app/common/services/content-management.service.ts b/src/app/services/content-management.service.ts similarity index 96% rename from src/app/common/services/content-management.service.ts rename to src/app/services/content-management.service.ts index 8dab2870fb..8f1eadb16b 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/services/content-management.service.ts @@ -27,10 +27,10 @@ import { Subject } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material'; import { FolderDialogComponent } from '@alfresco/adf-content-services'; -import { LibraryDialogComponent } from '../../dialogs/library/library.dialog'; -import { SnackbarErrorAction } from '../../store/actions'; +import { LibraryDialogComponent } from '../dialogs/library/library.dialog'; +import { SnackbarErrorAction } from '../store/actions'; import { Store } from '@ngrx/store'; -import { AppStore } from '../../store/states'; +import { AppStore } from '../store/states'; import { MinimalNodeEntity, MinimalNodeEntryEntity, diff --git a/src/app/common/services/experimental-guard.service.ts b/src/app/services/experimental-guard.service.ts similarity index 93% rename from src/app/common/services/experimental-guard.service.ts rename to src/app/services/experimental-guard.service.ts index 237541cb75..d07b7439c5 100644 --- a/src/app/common/services/experimental-guard.service.ts +++ b/src/app/services/experimental-guard.service.ts @@ -1,7 +1,7 @@ import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; import { Injectable } from '@angular/core'; import { AppConfigService, StorageService } from '@alfresco/adf-core'; -import { environment } from '../../../environments/environment'; +import { environment } from '../../environments/environment'; @Injectable() export class ExperimentalGuard implements CanActivate { diff --git a/src/app/common/services/node-actions.service.spec.ts b/src/app/services/node-actions.service.spec.ts similarity index 99% rename from src/app/common/services/node-actions.service.spec.ts rename to src/app/services/node-actions.service.spec.ts index 9902a7ce39..d00f04ec41 100644 --- a/src/app/common/services/node-actions.service.spec.ts +++ b/src/app/services/node-actions.service.spec.ts @@ -30,8 +30,8 @@ import { AlfrescoApiService, TranslationService } from '@alfresco/adf-core'; import { DocumentListService } from '@alfresco/adf-content-services'; import { NodeActionsService } from './node-actions.service'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { AppTestingModule } from '../../testing/app-testing.module'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; class TestNode { entry?: MinimalNodeEntryEntity; diff --git a/src/app/common/services/node-actions.service.ts b/src/app/services/node-actions.service.ts similarity index 99% rename from src/app/common/services/node-actions.service.ts rename to src/app/services/node-actions.service.ts index 1d49bf082a..4e861f7053 100644 --- a/src/app/common/services/node-actions.service.ts +++ b/src/app/services/node-actions.service.ts @@ -30,7 +30,7 @@ import { Observable, Subject } from 'rxjs/Rx'; import { AlfrescoApiService, ContentService, DataColumn, TranslationService } from '@alfresco/adf-core'; import { DocumentListService, ContentNodeSelectorComponent, ContentNodeSelectorComponentData } from '@alfresco/adf-content-services'; import { MinimalNodeEntity, MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; -import { ContentApiService } from '../../services/content-api.service'; +import { ContentApiService } from '../services/content-api.service'; @Injectable() export class NodeActionsService { diff --git a/src/app/common/services/node-permission.service.spec.ts b/src/app/services/node-permission.service.spec.ts similarity index 100% rename from src/app/common/services/node-permission.service.spec.ts rename to src/app/services/node-permission.service.spec.ts diff --git a/src/app/common/services/node-permission.service.ts b/src/app/services/node-permission.service.ts similarity index 100% rename from src/app/common/services/node-permission.service.ts rename to src/app/services/node-permission.service.ts diff --git a/src/app/common/services/profile.resolver.ts b/src/app/services/profile.resolver.ts similarity index 91% rename from src/app/common/services/profile.resolver.ts rename to src/app/services/profile.resolver.ts index 429bd3c443..da69e7c744 100644 --- a/src/app/common/services/profile.resolver.ts +++ b/src/app/services/profile.resolver.ts @@ -28,9 +28,9 @@ import { Injectable } from '@angular/core'; import { Resolve, Router } from '@angular/router'; import { Person } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; -import { AppStore } from '../../store/states/app.state'; -import { SetUserAction } from '../../store/actions/user.actions'; -import { ContentApiService } from '../../services/content-api.service'; +import { AppStore } from '../store/states/app.state'; +import { SetUserAction } from '../store/actions'; +import { ContentApiService } from './content-api.service'; @Injectable() export class ProfileResolver implements Resolve { diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index 115846e16f..d9debf63e5 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -36,7 +36,7 @@ import { } from '../actions/snackbar.actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../states/app.state'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { ContentApiService } from '../../services/content-api.service'; @Injectable() diff --git a/src/app/store/effects/node.effects.ts b/src/app/store/effects/node.effects.ts index cf8eb91856..48cc2bcb75 100644 --- a/src/app/store/effects/node.effects.ts +++ b/src/app/store/effects/node.effects.ts @@ -43,7 +43,7 @@ import { CreateFolderAction, CREATE_FOLDER } from '../actions'; -import { ContentManagementService } from '../../common/services/content-management.service'; +import { ContentManagementService } from '../../services/content-management.service'; import { Observable } from 'rxjs/Rx'; import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models'; import { ContentApiService } from '../../services/content-api.service'; diff --git a/src/app/testing/app-testing.module.ts b/src/app/testing/app-testing.module.ts index 43d40f3adf..e4a56ccbea 100644 --- a/src/app/testing/app-testing.module.ts +++ b/src/app/testing/app-testing.module.ts @@ -55,9 +55,9 @@ import { DocumentListService } from '@alfresco/adf-content-services'; import { MaterialModule } from '../material.module'; -import { ContentManagementService } from '../common/services/content-management.service'; -import { NodeActionsService } from '../common/services/node-actions.service'; -import { NodePermissionService } from '../common/services/node-permission.service'; +import { ContentManagementService } from '../services/content-management.service'; +import { NodeActionsService } from '../services/node-actions.service'; +import { NodePermissionService } from '../services/node-permission.service'; import { ContentApiService } from '../services/content-api.service'; import { ExtensionService } from '../extensions/extension.service'; import { RuleService } from '../extensions/rules/rule.service'; From 37dc6286ccd8f55b641dba8b9c4a6191722a5405 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 11:23:33 +0100 Subject: [PATCH 033/146] disable font faces for pdf viewer --- src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.ts b/src/main.ts index 42b37f12df..f7c572d092 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,7 @@ import 'hammerjs'; import * as pdfjsLib from 'pdfjs-dist'; pdfjsLib.PDFJS.workerSrc = 'pdf.worker.js'; +pdfjsLib.PDFJS.disableFontFace = true; if (environment.production) { enableProdMode(); From a643e8efc1414904d45cdf8558522d241663abed Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 14:23:26 +0100 Subject: [PATCH 034/146] custom route reuse strategy (#515) --- src/app/app.module.ts | 4 +- src/app/app.routes.strategy.ts | 126 +++++++++++++++++++++++++++++++++ src/app/app.routes.ts | 3 +- 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/app/app.routes.strategy.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6c53b95a4b..e418f6c77d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,7 +25,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, RouteReuseStrategy } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TRANSLATION_PROVIDER, CoreModule, AppConfigService, DebugAppConfigService } from '@alfresco/adf-core'; @@ -83,6 +83,7 @@ import { SearchResultsRowComponent } from './components/search/search-results-ro import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; import { NodePermissionsDirective } from './directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; +import { AppRouteReuseStrategy } from './app.routes.strategy'; @NgModule({ imports: [ @@ -143,6 +144,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per ExperimentalDirective ], providers: [ + { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy }, { provide: AppConfigService, useClass: DebugAppConfigService }, { provide: TRANSLATION_PROVIDER, diff --git a/src/app/app.routes.strategy.ts b/src/app/app.routes.strategy.ts new file mode 100644 index 0000000000..601481aebd --- /dev/null +++ b/src/app/app.routes.strategy.ts @@ -0,0 +1,126 @@ +/*! + * @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 { + RouteReuseStrategy, + DetachedRouteHandle, + ActivatedRouteSnapshot +} from '@angular/router'; + +interface RouteData { + reuse: boolean; +} + +interface RouteInfo { + handle: DetachedRouteHandle; + data: RouteData; +} + +export class AppRouteReuseStrategy implements RouteReuseStrategy { + private routeCache = new Map(); + + shouldReuseRoute( + future: ActivatedRouteSnapshot, + curr: ActivatedRouteSnapshot + ): boolean { + const ret = future.routeConfig === curr.routeConfig; + if (ret) { + this.addRedirectsRecursively(future); // update redirects + } + return ret; + } + + shouldDetach(route: ActivatedRouteSnapshot): boolean { + const data = this.getRouteData(route); + return data && data.reuse; + } + + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { + const url = this.getFullRouteUrl(route); + const data = this.getRouteData(route); + this.routeCache.set(url, { handle, data }); + this.addRedirectsRecursively(route); + } + + shouldAttach(route: ActivatedRouteSnapshot): boolean { + const url = this.getFullRouteUrl(route); + return this.routeCache.has(url); + } + + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { + const url = this.getFullRouteUrl(route); + const data = this.getRouteData(route); + return data && data.reuse && this.routeCache.has(url) + ? this.routeCache.get(url).handle + : null; + } + + private addRedirectsRecursively(route: ActivatedRouteSnapshot): void { + const config = route.routeConfig; + if (config) { + if (!config.loadChildren) { + const routeFirstChild = route.firstChild; + const routeFirstChildUrl = routeFirstChild + ? this.getRouteUrlPaths(routeFirstChild).join('/') + : ''; + const childConfigs = config.children; + if (childConfigs) { + const childConfigWithRedirect = childConfigs.find( + c => c.path === '' && !!c.redirectTo + ); + if (childConfigWithRedirect) { + childConfigWithRedirect.redirectTo = routeFirstChildUrl; + } + } + } + route.children.forEach(childRoute => + this.addRedirectsRecursively(childRoute) + ); + } + } + + private getFullRouteUrl(route: ActivatedRouteSnapshot): string { + return this.getFullRouteUrlPaths(route) + .filter(Boolean) + .join('/'); + } + + private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] { + const paths = this.getRouteUrlPaths(route); + return route.parent + ? [...this.getFullRouteUrlPaths(route.parent), ...paths] + : paths; + } + + private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] { + return route.url.map(urlSegment => urlSegment.path); + } + + private getRouteData(route: ActivatedRouteSnapshot): RouteData { + return ( + route.routeConfig && (route.routeConfig.data as RouteData) + ); + } +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c7725a5bbd..c57ca2a573 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -245,7 +245,8 @@ export const APP_ROUTES: Routes = [ path: '', component: SearchResultsComponent, data: { - title: 'APP.BROWSE.SEARCH.TITLE' + title: 'APP.BROWSE.SEARCH.TITLE', + reuse: true } }, { From 36629adf99b30db1b30102076bc10a553c42c216 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 14:24:24 +0100 Subject: [PATCH 035/146] upgrade libs (#516) --- package-lock.json | 14 +++++++------- package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37c11f1f18..5cd445d130 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,11 +5,11 @@ "requires": true, "dependencies": { "@alfresco/adf-content-services": { - "version": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", - "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3.tgz", - "integrity": "sha512-qIYgXgIptndkAjVmVzWXRRp5RQA4S0zcOACBrStKDkivAS+WxXDVNN/ZVpT9hM8LucpniIaInTc9W7xSX4Irrw==", + "version": "2.5.0-337c7e3a9e3d75736279578c383ed01313066286", + "resolved": "https://registry.npmjs.org/@alfresco/adf-content-services/-/adf-content-services-2.5.0-337c7e3a9e3d75736279578c383ed01313066286.tgz", + "integrity": "sha512-g81YTYPk93hvJG47Aeb8I1Zsx4f1GnIYOQLyImVl8rD/9oO0Ul/wtt4c5Sznnfyfz+XNYTJ9lxrzAJ3BHSLrrw==", "requires": { - "@alfresco/adf-core": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", + "@alfresco/adf-core": "2.5.0-337c7e3a9e3d75736279578c383ed01313066286", "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", "@angular/common": "5.1.1", @@ -61,9 +61,9 @@ } }, "@alfresco/adf-core": { - "version": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", - "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3.tgz", - "integrity": "sha512-CsppN0mzq3tOrIe6H2XRCr8EtKrvhQ0TvxhV4lDn7kF2jztLOgVm6I6Gyjs7nTMqXecP/WXzkpTkU/stuJOQJA==", + "version": "2.5.0-337c7e3a9e3d75736279578c383ed01313066286", + "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-2.5.0-337c7e3a9e3d75736279578c383ed01313066286.tgz", + "integrity": "sha512-wa2QG2bJdcODSf1b1gW5mPgJUR9Vx/jGf0OygRPMnAtCWh9p/Rp51U873ZHZ5BAD64ec8KIZqMvEowiIK47G5w==", "requires": { "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", diff --git a/package.json b/package.json index 7b57f39afe..264a7278c7 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ }, "private": true, "dependencies": { - "@alfresco/adf-content-services": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", - "@alfresco/adf-core": "2.5.0-b051a2ebc880573de7c1025dff9345acfe8f36a3", + "@alfresco/adf-content-services": "2.5.0-337c7e3a9e3d75736279578c383ed01313066286", + "@alfresco/adf-core": "2.5.0-337c7e3a9e3d75736279578c383ed01313066286", "@angular/animations": "5.1.1", "@angular/cdk": "5.0.1", "@angular/common": "5.1.1", From af4089ae74ce602c9b2b111c79bb5dcd2462738f Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Wed, 18 Jul 2018 18:25:30 +0300 Subject: [PATCH 036/146] [ACA-1574] DocumentList - single click navigation (#513) * single click navigation * fix tests * allow action on row * use adf link class * update tests --- e2e/components/data-table/data-table.ts | 10 +++++++ e2e/suites/list-views/file-libraries.test.ts | 4 +-- src/app/components/files/files.component.html | 10 ++++--- src/app/components/files/files.component.ts | 2 +- .../libraries/libraries.component.html | 10 ++++--- .../libraries/libraries.component.spec.ts | 26 +++++-------------- .../libraries/libraries.component.ts | 8 +++--- 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/e2e/components/data-table/data-table.ts b/e2e/components/data-table/data-table.ts index c8ec0d9c7d..c9cdf9ed6c 100755 --- a/e2e/components/data-table/data-table.ts +++ b/e2e/components/data-table/data-table.ts @@ -44,6 +44,7 @@ export class DataTable extends Component { selectedRow: '.adf-datatable-row.is-selected', cell: '.adf-data-table-cell', locationLink: 'aca-location-link', + linkCell: '.adf-location-cell', selectedIcon: '.mat-icon', @@ -59,6 +60,7 @@ export class DataTable extends Component { body: ElementFinder = this.component.element(by.css(DataTable.selectors.body)); cell = by.css(DataTable.selectors.cell); locationLink = by.css(DataTable.selectors.locationLink); + linkCell: ElementFinder = this.component.element(by.css(DataTable.selectors.linkCell)); emptyList: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyListContainer)); emptyFolderDragAndDrop: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyFolderDragAndDrop)); emptyListTitle: ElementFinder = this.component.element(by.css(DataTable.selectors.emptyListTitle)); @@ -139,10 +141,18 @@ export class DataTable extends Component { return this.body.element(by.cssContainingText(`.adf-data-table-cell span`, name)); } + getRowLink(name: string): ElementFinder { + return this.body.element(by.cssContainingText(`.adf-data-table-cell a`, name)); + } + getItemNameTooltip(name: string): promise.Promise { return this.getRowName(name).getAttribute('title'); } + getLinkCellTooltip(name: string): promise.Promise { + return this.getRowLink(name).getAttribute('title'); + } + countRows(): promise.Promise { return this.getRows().count(); } diff --git a/e2e/suites/list-views/file-libraries.test.ts b/e2e/suites/list-views/file-libraries.test.ts index 3f6567dddb..0b0fba94a1 100755 --- a/e2e/suites/list-views/file-libraries.test.ts +++ b/e2e/suites/list-views/file-libraries.test.ts @@ -150,12 +150,12 @@ describe('File Libraries', () => { }); it('Tooltip for sites without description [C217096]', () => { - const tooltip = dataTable.getItemNameTooltip(sitePrivate); + const tooltip = dataTable.getLinkCellTooltip(sitePrivate); expect(tooltip).toBe(`${sitePrivate}`); }); it('Tooltip for sites with description [C217097]', () => { - const tooltip = dataTable.getItemNameTooltip(siteModerated); + const tooltip = dataTable.getLinkCellTooltip(siteModerated); expect(tooltip).toBe(`${siteDescription}`); }); }); diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index 00a54cfa4a..7fa555c0b9 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -147,7 +147,7 @@ [allowDropFiles]="true" [navigate]="false" [imageResolver]="imageResolver" - (node-dblclick)="onNodeDoubleClick($event.detail?.node)"> + (node-dblclick)="navigateTo($event.detail?.node)"> - {{ value }} + + {{ value }} + diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index 22efb389a3..cce77e0588 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -118,7 +118,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { }); } - onNodeDoubleClick(node: MinimalNodeEntity) { + navigateTo(node: MinimalNodeEntity) { if (node && node.entry) { const { id, isFolder } = node.entry; diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index 932eef78fc..b8d57991d0 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -49,7 +49,7 @@ selectionMode="single" [navigate]="false" [sorting]="[ 'title', 'asc' ]" - (node-dblclick)="onNodeDoubleClick($event)"> + (node-dblclick)="navigateTo($event.detail?.node)"> @@ -70,13 +70,15 @@ - + {{ makeLibraryTitle(context.row.obj.entry) }} - + diff --git a/src/app/components/libraries/libraries.component.spec.ts b/src/app/components/libraries/libraries.component.spec.ts index 5137e2992a..a9edcd5db9 100644 --- a/src/app/components/libraries/libraries.component.spec.ts +++ b/src/app/components/libraries/libraries.component.spec.ts @@ -163,35 +163,23 @@ describe('LibrariesComponent', () => { }); }); - describe('onNodeDoubleClick', () => { - it('navigates to document', () => { + describe('navigateTo', () => { + it('navigates into library folder', () => { spyOn(component, 'navigate'); - const event: any = { - detail: { - node: { - entry: { guid: 'node-guid' } - } - } + const site: any = { + entry: { guid: 'node-guid' } }; - component.onNodeDoubleClick(event); + component.navigateTo(site); expect(component.navigate).toHaveBeenCalledWith('node-guid'); }); - it(' does not navigate when document is not provided', () => { + it(' does not navigate when library is not provided', () => { spyOn(component, 'navigate'); - const event: any = { - detail: { - node: { - entry: null - } - } - }; - - component.onNodeDoubleClick(event); + component.navigateTo(null); expect(component.navigate).not.toHaveBeenCalled(); }); diff --git a/src/app/components/libraries/libraries.component.ts b/src/app/components/libraries/libraries.component.ts index 841b1e3933..919bef6d84 100644 --- a/src/app/components/libraries/libraries.component.ts +++ b/src/app/components/libraries/libraries.component.ts @@ -84,11 +84,9 @@ export class LibrariesComponent extends PageComponent implements OnInit { return isDuplicate ? `${title} (${id})` : `${title}`; } - onNodeDoubleClick(e: CustomEvent) { - const node: any = e.detail.node.entry; - - if (node && node.guid) { - this.navigate(node.guid); + navigateTo(node: SiteEntry) { + if (node && node.entry.guid) { + this.navigate(node.entry.guid); } } From cc63352e49135afbbe2f5116e8c92b7f804ec936 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Wed, 18 Jul 2018 19:33:43 +0300 Subject: [PATCH 037/146] [ACA-1587] viewer toolbar theme (#517) --- src/app/ui/overrides/adf-toolbar.theme.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/ui/overrides/adf-toolbar.theme.scss b/src/app/ui/overrides/adf-toolbar.theme.scss index b2fa28225d..e42783ce72 100644 --- a/src/app/ui/overrides/adf-toolbar.theme.scss +++ b/src/app/ui/overrides/adf-toolbar.theme.scss @@ -1,4 +1,16 @@ @mixin adf-toolbar-theme($theme) { + $foreground: map-get($theme, foreground); + + .adf-viewer { + @include angular-material-theme($theme); + + .adf-toolbar { + .mat-toolbar { + color: mat-color($foreground, text, .54); + + } + } + } .adf-toolbar { @include angular-material-theme($theme); From 1ee92fd6bc4e3e9b0cfeb0443c7f453d54a4aab6 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Thu, 19 Jul 2018 08:49:25 +0300 Subject: [PATCH 038/146] name column link (#519) --- src/app/components/favorites/favorites.component.html | 8 ++++++-- .../components/recent-files/recent-files.component.html | 8 ++++++-- .../components/shared-files/shared-files.component.html | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 4105319d7b..67eceaca60 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -153,11 +153,15 @@ - {{ value }} + + {{ value }} + diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index 1f6c334de4..a56b13463f 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -147,11 +147,15 @@ - {{ value }} + + {{ value }} + diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 786766de15..0cd9946fcb 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -142,11 +142,15 @@ - {{ value }} + + {{ value }} + From 43a71aa1c8ac066df7f3cfac6e996162eb49e4ec Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Thu, 19 Jul 2018 16:03:07 +0300 Subject: [PATCH 039/146] [ACA-1574] Search - DL single click for folders (#520) --- .../search-results-row/search-results-row.component.html | 2 +- .../search-results-row/search-results-row.component.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/components/search/search-results-row/search-results-row.component.html b/src/app/components/search/search-results-row/search-results-row.component.html index af54c8ef39..8fc3fe23c0 100644 --- a/src/app/components/search/search-results-row/search-results-row.component.html +++ b/src/app/components/search/search-results-row/search-results-row.component.html @@ -1,6 +1,6 @@
{{ name }} - {{ name }} + {{ name }} ( {{ title }} )
diff --git a/src/app/components/search/search-results-row/search-results-row.component.ts b/src/app/components/search/search-results-row/search-results-row.component.ts index 163b47a92d..2dd2dde48b 100644 --- a/src/app/components/search/search-results-row/search-results-row.component.ts +++ b/src/app/components/search/search-results-row/search-results-row.component.ts @@ -28,6 +28,7 @@ import { MinimalNodeEntity } from 'alfresco-js-api'; import { ViewFileAction } from '../../../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../../../store/states/app.state'; +import { NavigateToFolder } from '../../../store/actions'; @Component({ selector: 'aca-search-results-row', @@ -94,6 +95,10 @@ export class SearchResultsRowComponent implements OnInit { ); } + navigate() { + this.store.dispatch(new NavigateToFolder(this.node)); + } + private getValue(path) { return path .replace('["', '.') From 8c9ffc11605542a422dd7c199d2b65b2e7599efd Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Thu, 19 Jul 2018 20:54:39 +0100 Subject: [PATCH 040/146] [ACA-1591] Load extensions from multiple files (#521) * rework extension service, separate file with config * improve loading, optional entries * simplify config and unify content actions * load and merge multiple files * improve plugin loading, introduce second demo * move demo stuff to a plugin * rework navbar to make it pluggable * code and naming convention cleanup * extension schema * switch off custom navbar group by default * hotfix for facetQueries issue * consolidate files, final renames --- extension.schema.json | 300 +++++++++++++ src/app.config.json | 270 +---------- src/app/app.component.ts | 2 - src/app/app.module.ts | 16 +- src/app/components/page.component.ts | 4 +- .../components/preview/preview.component.html | 2 +- .../components/preview/preview.component.ts | 4 +- .../components/sidenav/sidenav.component.html | 6 +- .../components/sidenav/sidenav.component.ts | 10 +- ...tion.extension.ts => action.extensions.ts} | 15 +- src/app/extensions/actions/action-ref.ts | 30 -- .../extensions/actions/action.service.spec.ts | 141 ------ src/app/extensions/actions/action.service.ts | 76 ---- .../toolbar-action.component.ts | 4 +- src/app/extensions/core.extensions.module.ts | 29 +- .../{rules => evaluators}/app.evaluators.ts | 3 +- .../{rules => evaluators}/core.evaluators.ts | 3 +- src/app/extensions/extension.config.ts | 21 +- src/app/extensions/extension.service.spec.ts | 425 ++++++++++-------- src/app/extensions/extension.service.ts | 355 ++++++++++----- ...tion.extension.ts => navbar.extensions.ts} | 14 +- src/app/extensions/open-with.extension.ts | 33 -- .../{route-ref.ts => routing.extensions.ts} | 5 +- .../rule-context.ts => rule.extensions.ts} | 18 +- src/app/extensions/rules/rule-parameter.ts | 29 -- src/app/extensions/rules/rule-ref.ts | 34 -- src/app/extensions/rules/rule.service.ts | 97 ---- src/app/store/selectors/app.selectors.ts | 11 + src/app/testing/app-testing.module.ts | 6 +- src/assets/app.extensions.json | 189 ++++++++ src/assets/plugins/.gitkeep | 0 src/assets/plugins/plugin1.json | 66 +++ src/assets/plugins/plugin2.json | 42 ++ tslint.json | 1 - 34 files changed, 1211 insertions(+), 1050 deletions(-) create mode 100644 extension.schema.json rename src/app/extensions/{content-action.extension.ts => action.extensions.ts} (88%) delete mode 100644 src/app/extensions/actions/action-ref.ts delete mode 100644 src/app/extensions/actions/action.service.spec.ts delete mode 100644 src/app/extensions/actions/action.service.ts rename src/app/extensions/{rules => evaluators}/app.evaluators.ts (96%) rename src/app/extensions/{rules => evaluators}/core.evaluators.ts (94%) rename src/app/extensions/{navigation.extension.ts => navbar.extensions.ts} (83%) delete mode 100644 src/app/extensions/open-with.extension.ts rename src/app/extensions/{route-ref.ts => routing.extensions.ts} (96%) rename src/app/extensions/{rules/rule-context.ts => rule.extensions.ts} (77%) delete mode 100644 src/app/extensions/rules/rule-parameter.ts delete mode 100644 src/app/extensions/rules/rule-ref.ts delete mode 100644 src/app/extensions/rules/rule.service.ts create mode 100644 src/assets/app.extensions.json create mode 100644 src/assets/plugins/.gitkeep create mode 100644 src/assets/plugins/plugin1.json create mode 100644 src/assets/plugins/plugin2.json diff --git a/extension.schema.json b/extension.schema.json new file mode 100644 index 0000000000..7989236ba7 --- /dev/null +++ b/extension.schema.json @@ -0,0 +1,300 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/Alfresco/alfresco-content-app/blob/development/extension.schema.json", + "title": "ACA Extension Schema", + "description": "Provides a validation schema for ACA extensions", + + "definitions": { + "ruleRef": { + "type": "object", + "required": ["id", "type"], + "properties": { + "id": { + "description": "Unique rule definition id", + "type": "string" + }, + "type": { + "description": "Rule evaluator type", + "type": "string" + }, + "parameters": { + "description": "Rule evaluator parameters", + "type": "array", + "items": { "$ref": "#/definitions/ruleParameter" }, + "minItems": 1 + } + } + }, + "ruleParameter": { + "type": "object", + "required": ["type", "value"], + "properties": { + "type": { + "description": "Rule parameter type", + "type": "string" + }, + "value": { + "description": "Rule parameter value", + "type": "string" + } + } + }, + "routeRef": { + "type": "object", + "required": ["id", "path", "component"], + "properties": { + "id": { + "description": "Unique route reference identifier.", + "type": "string" + }, + "path": { + "description": "Route path to register.", + "type": "string" + }, + "component": { + "description": "Unique identifier for the Component to use with the route.", + "type": "string" + }, + "layout": { + "description": "Unique identifier for the custom layout component to use.", + "type": "string" + }, + "auth": { + "description": "List of the authentication guards to use with the route.", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true + }, + "data": { + "description": "Custom data to pass to the activated route so that your components can access it", + "type": "object" + } + } + }, + "actionRef": { + "type": "object", + "required": ["id", "type"], + "properties": { + "id": { + "description": "Unique action identifier", + "type": "string" + }, + "type": { + "description": "Action type", + "type": "string" + }, + "payload": { + "description": "Action payload value (string or expression)", + "type": "string" + } + } + }, + "contentActionRef": { + "type": "object", + "required": ["id", "type"], + "properties": { + "id": { + "description": "Unique action identifier.", + "type": "string" + }, + "type": { + "description": "Element type", + "type": "string", + "enum": ["default", "button", "separator", "menu"] + }, + "title": { + "description": "Element title", + "type": "string" + }, + "order": { + "description": "Element order", + "type": "number" + }, + "icon": { + "description": "Element icon", + "type": "string" + }, + "disabled": { + "description": "Toggles disabled state", + "type": "boolean" + }, + "children": { + "description": "Child entries for the container types.", + "type": "array", + "items": { "$ref": "#/definitions/contentActionRef" }, + "minItems": 1 + }, + "actions": { + "description": "Element actions", + "type": "object", + "properties": { + "click": { + "description": "Action reference for the click handler", + "type": "string" + } + } + }, + "rules": { + "description": "Element rules", + "type": "object", + "properties": { + "enabled": { + "description": "Rule to evaluate the enabled state", + "type": "string" + }, + "visible": { + "description": "Rule to evaluate the visibility state", + "type": "string" + } + } + } + } + }, + "navBarLinkRef": { + "type": "object", + "required": ["id", "icon", "title", "route"], + "properties": { + "id": { + "description": "Unique identifier", + "type": "string" + }, + "icon": { + "description": "Element icon", + "type": "string" + }, + "title": { + "description": "Element title", + "type": "string" + }, + "route": { + "description": "Route reference identifier", + "type": "string" + }, + "description": { + "description": "Element description or tooltip", + "type": "string" + }, + "order": { + "description": "Element order", + "type": "number" + }, + "disabled": { + "description": "Toggles the disabled state", + "type": "boolean" + } + } + }, + "navBarGroupRef": { + "type": "object", + "required": ["id", "items"], + "properties": { + "id": { + "description": "Unique identifier for the navigation group", + "type": "string" + }, + "items": { + "description": "Navigation group items", + "type": "array", + "items": { "$ref": "#/definitions/navBarLinkRef" }, + "minItems": 1 + }, + "order": { + "description": "Group order", + "type": "number" + }, + "disabled": { + "description": "Toggles the disabled state", + "type": "boolean" + } + } + } + }, + + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { + "description": "Extension name", + "type": "string" + }, + "version": { + "description": "Extension version", + "type": "string" + }, + "description": { + "description": "Brief description on what the extension does" + }, + "references": { + "description": "References to external files", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "rules": { + "description": "List of rule definitions", + "type": "array", + "items": { "$ref": "#/definitions/ruleRef" }, + "minItems": 1 + }, + "routes": { + "description": "List of custom application routes", + "type": "array", + "items": { "$ref": "#/definitions/routeRef" }, + "minItems": 1 + }, + "actions": { + "description": "List of action definitions", + "type": "array", + "items": { "$ref": "#/definitions/actionRef" }, + "minItems": 1 + }, + "features": { + "description": "Application-specific features and extensions", + "type": "object", + "properties": { + "create": { + "description": "The [New] menu component extensions", + "type": "array", + "items": { "$ref": "#/definitions/contentActionRef" }, + "minItems": 1 + }, + "viewer": { + "description": "Viewer component extensions", + "type": "object", + "properties": { + "openWith": { + "description": "The [Open With] menu extensions", + "type": "array", + "items": { "$ref": "#/definitions/contentActionRef" }, + "minItems": 1 + } + } + }, + "navbar": { + "description": "Navigation bar extensions", + "type": "array", + "items": { "$ref": "#/definitions/navBarGroupRef" }, + "minItems": 1 + }, + "content": { + "description": "Main application content extensions", + "type": "object", + "properties": { + "actions": { + "description": "Content actions (toolbar, context menus, etc.)", + "type": "array", + "items": { "$ref": "#/definitions/contentActionRef" }, + "minItems": 1 + } + } + } + } + } + } +} diff --git a/src/app.config.json b/src/app.config.json index bbccd407a3..a7f3707d22 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -36,275 +36,6 @@ "preserveState": true, "expandedSidenav": true }, - "extensions": { - "external": [ - "plugin1.json", - "plugin2.json" - ], - "core": { - "rules": [ - { - "id": "app.create.canCreateFolder", - "type": "app.navigation.folder.canCreate" - }, - { - "id": "app.toolbar.canEditFolder", - "type": "core.every", - "parameters": [ - { "type": "rule", "value": "app.selection.folder" }, - { "type": "rule", "value": "app.selection.folder.canUpdate" } - ] - }, - { - "id": "app.toolbar.canViewFile", - "type": "app.selection.file" - }, - { - "id": "app.toolbar.canDownload", - "type": "app.selection.canDownload" - } - ], - "routes": [ - { - "id": "aca:routes/about", - "path": "ext/about", - "component": "aca:components/about", - "layout": "aca:layouts/main", - "auth":[ "aca:auth" ], - "data": { - "title": "Custom About" - } - } - ], - "actions": [ - { - "id": "aca:actions/create-folder", - "type": "CREATE_FOLDER", - "payload": null - }, - { - "id": "aca:actions/edit-folder", - "type": "EDIT_FOLDER", - "payload": null - }, - { - "id": "aca:actions/download", - "type": "DOWNLOAD_NODES", - "payload": null - }, - { - "id": "aca:actions/preview", - "type": "VIEW_FILE", - "payload": null - }, - - { - "id": "aca:actions/info", - "type": "SNACKBAR_INFO", - "payload": "I'm a nice little popup raised by extension." - }, - { - "id": "aca:actions/node-name", - "type": "SNACKBAR_INFO", - "payload": "$('Action for ' + context.selection.first.entry.name)" - }, - { - "id": "aca:actions/settings", - "type": "NAVIGATE_URL", - "payload": "/settings" - } - ], - "features": { - "create": [ - { - "id": "app.create.folder", - "icon": "create_new_folder", - "title": "ext: Create Folder", - "actions": { - "click": "aca:actions/create-folder" - }, - "rules": { - "enabled": "app.create.canCreateFolder" - } - } - ], - "navigation": { - "aca:main": [ - { - "id": "aca/personal-files", - "order": 100, - "icon": "folder", - "title": "APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP", - "route": "personal-files" - }, - { - "id": "aca/libraries", - "order": 101, - "icon": "group_work", - "title": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP", - "route": "libraries" - } - ], - "aca:secondary": [ - { - "id": "aca/shared", - "order": 100, - "icon": "people", - "title": "APP.BROWSE.SHARED.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP", - "route": "shared" - }, - { - "id": "aca/recent-files", - "order": 101, - "icon": "schedule", - "title": "APP.BROWSE.RECENT.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP", - "route": "recent-files" - }, - { - "id": "aca/favorites", - "order": 102, - "icon": "star", - "title": "APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP", - "route": "favorites" - }, - { - "id": "aca/trashcan", - "order": 103, - "icon": "delete", - "title": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL", - "description": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP", - "route": "trashcan" - } - ], - "aca:demo": [ - { - "disabled": true, - "id": "aca:demo/link1", - "order": 100, - "icon": "build", - "title": "About (native)", - "description": "Uses native application route", - "route": "about" - }, - { - "disabled": true, - "id": "aca:demo/link2", - "order": 100, - "icon": "build", - "title": "About (custom)", - "description": "Uses custom defined route", - "route": "aca:routes/about" - } - ] - }, - "viewer": { - "open-with": [ - { - "disabled": false, - "id": "aca:viewer/action1", - "order": 100, - "icon": "build", - "title": "Snackbar", - "action": "aca:actions/info" - } - ] - }, - "content": { - "actions": [ - { - "id": "aca:toolbar/separator-1", - "order": 5, - "type": "separator" - }, - { - "id": "aca:toolbar/create-folder", - "type": "button", - "order": 10, - "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", - "icon": "create_new_folder", - "actions": { - "click": "aca:actions/create-folder" - }, - "rules": { - "visible": "app.create.canCreateFolder" - } - }, - { - "id": "aca:toolbar/preview", - "type": "button", - "order": 15, - "title": "APP.ACTIONS.VIEW", - "icon": "open_in_browser", - "actions": { - "click": "aca:actions/preview" - }, - "rules": { - "visible": "app.toolbar.canViewFile" - } - }, - { - "id": "aca:toolbar/download", - "type": "button", - "order": 20, - "title": "APP.ACTIONS.DOWNLOAD", - "icon": "get_app", - "actions": { - "click": "aca:actions/download" - }, - "rules": { - "visible": "app.toolbar.canDownload" - } - }, - { - "id": "aca:toolbar/edit-folder", - "type": "button", - "order": 30, - "title": "APP.ACTIONS.EDIT", - "icon": "create", - "actions": { - "click": "aca:actions/edit-folder" - }, - "rules": { - "visible": "app.toolbar.canEditFolder" - } - }, - - { - "id": "aca:toolbar/separator-2", - "order": 200, - "type": "separator" - }, - { - "id": "aca:toolbar/menu-1", - "type": "menu", - "icon": "storage", - "order": 300, - "children": [ - { - "id": "aca:action3", - "type": "button", - "title": "Settings", - "icon": "settings_applications", - "actions": { - "click": "aca:actions/settings" - } - } - ] - }, - { - "id": "aca:toolbar/separator-3", - "type": "separator" - } - ] - } - } - } - }, "languages": [ { "key": "de", @@ -460,6 +191,7 @@ { "field": "modifier", "mincount": 1, "label": "SEARCH.FACET_FIELDS.MODIFIER" }, { "field": "SITE", "mincount": 1, "label": "SEARCH.FACET_FIELDS.FILE_LIBRARY" } ], + "facetQueries": {}, "categories": [ { "id": "modifiedDate", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 963f6ce14f..30a1c2292e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -91,8 +91,6 @@ export class AppComponent implements OnInit { pageTitle.setTitle(data.title || ''); }); - this.extensions.init(); - this.router.config.unshift(...this.extensions.getApplicationRoutes()); this.uploadService.fileUploadError.subscribe(error => diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e418f6c77d..49cdd7502c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -24,7 +24,7 @@ */ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { NgModule, APP_INITIALIZER } from '@angular/core'; import { RouterModule, RouteReuseStrategy } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -84,7 +84,11 @@ import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node- import { NodePermissionsDirective } from './directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; import { AppRouteReuseStrategy } from './app.routes.strategy'; +import { ExtensionService } from './extensions/extension.service'; +export function setupExtensionServiceFactory(service: ExtensionService): Function { + return () => service.load(); +} @NgModule({ imports: [ BrowserModule, @@ -96,7 +100,7 @@ import { AppRouteReuseStrategy } from './app.routes.strategy'; enableTracing: false // enable for debug only }), MaterialModule, - CoreModule, + CoreModule.forRoot(), ContentModule, AppStoreModule, CoreExtensionsModule, @@ -159,7 +163,13 @@ import { AppRouteReuseStrategy } from './app.routes.strategy'; NodePermissionService, ProfileResolver, ExperimentalGuard, - ContentApiService + ContentApiService, + { + provide: APP_INITIALIZER, + useFactory: setupExtensionServiceFactory, + deps: [ExtensionService], + multi: true + } ], entryComponents: [ LibraryDialogComponent, diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts index c70c169d68..ac20b941d1 100644 --- a/src/app/components/page.component.ts +++ b/src/app/components/page.component.ts @@ -36,8 +36,8 @@ import { AppStore } from '../store/states/app.state'; import { SelectionState } from '../store/states/selection.state'; import { Observable } from 'rxjs/Rx'; import { ExtensionService } from '../extensions/extension.service'; -import { ContentActionExtension } from '../extensions/content-action.extension'; import { ContentManagementService } from '../services/content-management.service'; +import { ContentActionRef } from '../extensions/action.extensions'; export abstract class PageComponent implements OnInit, OnDestroy { @@ -52,7 +52,7 @@ export abstract class PageComponent implements OnInit, OnDestroy { selection: SelectionState; displayMode = DisplayMode.List; sharedPreviewUrl$: Observable; - actions: Array = []; + actions: Array = []; canUpdateFile = false; canUpdateNode = false; canDelete = false; diff --git a/src/app/components/preview/preview.component.html b/src/app/components/preview/preview.component.html index 8dd91b2dce..4497904a10 100644 --- a/src/app/components/preview/preview.component.html +++ b/src/app/components/preview/preview.component.html @@ -18,7 +18,7 @@ diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index eb30884765..9affe06745 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -32,8 +32,8 @@ import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; -import { OpenWithExtension } from '../../extensions/open-with.extension'; import { ContentManagementService } from '../../services/content-management.service'; +import { ContentActionRef } from '../../extensions/action.extensions'; @Component({ selector: 'app-preview', templateUrl: 'preview.component.html', @@ -52,7 +52,7 @@ export class PreviewComponent extends PageComponent implements OnInit { previousNodeId: string; nextNodeId: string; navigateMultiple = false; - openWith: Array = []; + openWith: Array = []; constructor( private contentApi: ContentApiService, diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 042d0b5ba3..4a190d3c49 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -59,13 +59,13 @@
    -
  • + ` +}) +export class DemoButtonComponent {} diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html index 9684bf899b..b69d1f8d41 100644 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.html +++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.html @@ -6,7 +6,9 @@ (click)="runAction(entry.actions.click)"> {{ entry.icon }} + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
-
+
diff --git a/src/app/components/favorites/favorites.component.ts b/src/app/components/favorites/favorites.component.ts index da332aa3fa..33da9d6d80 100644 --- a/src/app/components/favorites/favorites.component.ts +++ b/src/app/components/favorites/favorites.component.ts @@ -59,7 +59,8 @@ export class FavoritesComponent extends PageComponent implements OnInit { this.content.nodesDeleted.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()), this.content.folderEdited.subscribe(() => this.reload()), - this.content.nodesMoved.subscribe(() => this.reload()) + this.content.nodesMoved.subscribe(() => this.reload()), + this.content.favoriteRemoved.subscribe(() => this.reload()) ]); } diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html index e163ac382d..8601d3d448 100644 --- a/src/app/components/files/files.component.html +++ b/src/app/components/files/files.component.html @@ -7,125 +7,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
@@ -141,6 +25,7 @@ [disabled]="!canUpload"> -
+
diff --git a/src/app/components/files/files.component.spec.ts b/src/app/components/files/files.component.spec.ts index a8f706818a..0788d3f39d 100644 --- a/src/app/components/files/files.component.spec.ts +++ b/src/app/components/files/files.component.spec.ts @@ -29,7 +29,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { TimeAgoPipe, NodeNameTooltipPipe, FileSizePipe, NodeFavoriteDirective, - DataTableComponent, UploadService, AppConfigPipe + DataTableComponent, + UploadService, + AppConfigPipe } from '@alfresco/adf-core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; import { ContentManagementService } from '../../services/content-management.service'; @@ -41,7 +43,6 @@ import { ExperimentalDirective } from '../../directives/experimental.directive'; describe('FilesComponent', () => { let node; - let page; let fixture: ComponentFixture; let component: FilesComponent; let contentManagementService: ContentManagementService; @@ -90,20 +91,12 @@ describe('FilesComponent', () => { beforeEach(() => { node = { id: 'node-id', isFolder: true }; - page = { - list: { - entries: ['a', 'b', 'c'], - pagination: {} - } - }; - spyOn(component.documentList, 'loadFolder').and.callFake(() => {}); }); describe('Current page is valid', () => { it('should be a valid current page', fakeAsync(() => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); - spyOn(component, 'fetchNodes').and.returnValue(Observable.throw(null)); + spyOn(contentApi, 'getNode').and.returnValue(Observable.throw(null)); component.ngOnInit(); fixture.detectChanges(); @@ -114,7 +107,6 @@ describe('FilesComponent', () => { it('should set current page as invalid path', fakeAsync(() => { spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); component.ngOnInit(); tick(); @@ -127,22 +119,10 @@ describe('FilesComponent', () => { describe('OnInit', () => { it('should set current node', () => { spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); - fixture.detectChanges(); - expect(component.node).toBe(node); }); - it('should get current node children', () => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); - - fixture.detectChanges(); - - expect(component.fetchNodes).toHaveBeenCalled(); - }); - it('if should navigate to parent if node is not a folder', () => { node.isFolder = false; node.parentId = 'parent-id'; @@ -157,8 +137,7 @@ describe('FilesComponent', () => { describe('refresh on events', () => { beforeEach(() => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node)); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); + spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); spyOn(component.documentList, 'reload'); fixture.detectChanges(); @@ -170,9 +149,9 @@ describe('FilesComponent', () => { { entry: { parentId: '2' } } ]; - component.node = { id: '1' }; + component.node = { id: '1' }; - nodeActionsService.contentCopied.next(nodes); + nodeActionsService.contentCopied.next(nodes); expect(component.documentList.reload).toHaveBeenCalled(); }); @@ -183,9 +162,9 @@ describe('FilesComponent', () => { { entry: { parentId: '2' } } ]; - component.node = { id: '3' }; + component.node = { id: '3' }; - nodeActionsService.contentCopied.next(nodes); + nodeActionsService.contentCopied.next(nodes); expect(component.documentList.reload).not.toHaveBeenCalled(); }); @@ -222,7 +201,7 @@ describe('FilesComponent', () => { it('should call refresh on fileUploadComplete event if parent node match', () => { const file = { file: { options: { parentId: 'parentId' } } }; - component.node = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadComplete.next(file); @@ -231,7 +210,7 @@ describe('FilesComponent', () => { it('should not call refresh on fileUploadComplete event if parent mismatch', () => { const file = { file: { options: { parentId: 'otherId' } } }; - component.node = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadComplete.next(file); @@ -240,7 +219,7 @@ describe('FilesComponent', () => { it('should call refresh on fileUploadDeleted event if parent node match', () => { const file = { file: { options: { parentId: 'parentId' } } }; - component.node = { id: 'parentId' }; + component.node = { id: 'parentId' }; uploadService.fileUploadDeleted.next(file); @@ -248,40 +227,24 @@ describe('FilesComponent', () => { }); it('should not call refresh on fileUploadDeleted event if parent mismatch', () => { - const file = { file: { options: { parentId: 'otherId' } } }; - component.node = { id: 'parentId' }; + const file: any = { file: { options: { parentId: 'otherId' } } }; + component.node = { id: 'parentId' }; - uploadService.fileUploadDeleted.next(file); + uploadService.fileUploadDeleted.next(file); expect(component.documentList.reload).not.toHaveBeenCalled(); }); }); - describe('fetchNodes()', () => { - beforeEach(() => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node)); - spyOn(contentApi, 'getNodeChildren').and.returnValue(Observable.of(page)); - - fixture.detectChanges(); - }); - - it('should call getNode api with node id', () => { - component.fetchNodes('nodeId'); - - expect(contentApi.getNodeChildren).toHaveBeenCalledWith('nodeId'); - }); - }); describe('onBreadcrumbNavigate()', () => { beforeEach(() => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node)); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); - + spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); fixture.detectChanges(); }); it('should navigates to node id', () => { - const routeData = { id: 'some-where-over-the-rainbow' }; + const routeData: any = { id: 'some-where-over-the-rainbow' }; spyOn(component, 'navigate'); component.onBreadcrumbNavigate(routeData); @@ -292,8 +255,7 @@ describe('FilesComponent', () => { describe('Node navigation', () => { beforeEach(() => { - spyOn(contentApi, 'getNode').and.returnValue(Observable.of(node)); - spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page)); + spyOn(contentApi, 'getNode').and.returnValue(Observable.of({ entry: node })); spyOn(router, 'navigate'); fixture.detectChanges(); @@ -312,7 +274,7 @@ describe('FilesComponent', () => { }); it('should navigate home if node is root', () => { - (component).node = { + component.node = { path: { elements: [ {id: 'node-id'} ] } diff --git a/src/app/components/files/files.component.ts b/src/app/components/files/files.component.ts index cce77e0588..98078cd80e 100644 --- a/src/app/components/files/files.component.ts +++ b/src/app/components/files/files.component.ts @@ -27,8 +27,7 @@ import { FileUploadEvent, UploadService } from '@alfresco/adf-core'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, PathElement, PathElementEntity } from 'alfresco-js-api'; -import { Observable } from 'rxjs/Rx'; +import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElement, PathElementEntity } from 'alfresco-js-api'; import { ContentManagementService } from '../../services/content-management.service'; import { NodeActionsService } from '../../services/node-actions.service'; import { AppStore } from '../../store/states/app.state'; @@ -68,19 +67,21 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { route.params.subscribe(({ folderId }: Params) => { const nodeId = folderId || data.defaultNodeId; - this.contentApi.getNode(nodeId) - .map(node => node.entry) - .do(node => { - if (node.isFolder) { - this.updateCurrentNode(node); - } else { - this.router.navigate(['/personal-files', node.parentId], { replaceUrl: true }); - } - }) - .skipWhile(node => !node.isFolder) - .flatMap(node => this.fetchNodes(node.id)) + this.contentApi + .getNode(nodeId) .subscribe( - () => this.isValidPath = true, + node => { + this.isValidPath = true; + + if (node.entry && node.entry.isFolder) { + this.updateCurrentNode(node.entry); + } else { + this.router.navigate( + ['/personal-files', node.entry.parentId], + { replaceUrl: true } + ); + } + }, () => this.isValidPath = false ); }); @@ -102,10 +103,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy { this.store.dispatch(new SetCurrentFolderAction(null)); } - fetchNodes(parentNodeId?: string): Observable { - return this.contentApi.getNodeChildren(parentNodeId); - } - navigate(nodeId: string = null) { const commands = [ './' ]; diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index c140731b47..69cb63d401 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -4,39 +4,11 @@ - + - - - - - - - - + + + @@ -45,6 +17,7 @@
; node: MinimalNodeEntryEntity; selection: SelectionState; - displayMode = DisplayMode.List; + documentDisplayMode$: Observable; sharedPreviewUrl$: Observable; actions: Array = []; - canUpdateFile = false; + viewerActions: Array = []; canUpdateNode = false; - canDelete = false; - canEditFolder = false; canUpload = false; - canDeleteShared = false; - canUpdateShared = false; protected subscriptions: Subscription[] = []; @@ -74,22 +69,17 @@ export abstract class PageComponent implements OnInit, OnDestroy { ngOnInit() { this.sharedPreviewUrl$ = this.store.select(sharedUrl); + this.infoDrawerOpened$ = this.store.select(infoDrawerOpened); + this.documentDisplayMode$ = this.store.select(documentDisplayMode); this.store .select(appSelection) .pipe(takeUntil(this.onDestroy$)) .subscribe(selection => { this.selection = selection; - if (selection.isEmpty) { - this.infoDrawerOpened = false; - } this.actions = this.extensions.getAllowedContentActions(); - this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file); + this.viewerActions = this.extensions.getViewerActions(); this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first); - this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes); - this.canEditFolder = selection.folder && this.content.canUpdateNode(selection.folder); - this.canDeleteShared = !this.selection.isEmpty && this.content.canDeleteSharedNodes(selection.nodes); - this.canUpdateShared = selection.file && this.content.canUpdateSharedNode(selection.file); }); this.store.select(currentFolder) @@ -127,14 +117,6 @@ export abstract class PageComponent implements OnInit, OnDestroy { return null; } - toggleSidebar(event) { - if (event) { - return; - } - - this.infoDrawerOpened = !this.infoDrawerOpened; - } - reload(): void { if (this.documentList) { this.documentList.resetSelection(); @@ -143,22 +125,7 @@ export abstract class PageComponent implements OnInit, OnDestroy { } } - toggleGalleryView(): void { - this.displayMode = this.displayMode === DisplayMode.List ? DisplayMode.Gallery : DisplayMode.List; - this.documentList.display = this.displayMode; - } - - downloadSelection() { - this.store.dispatch(new DownloadNodesAction()); - } - - // this is where each application decides how to treat an action and what to do - // the ACA maps actions to the NgRx actions as an example - runAction(actionId: string) { - const context = { - selection: this.selection - }; - - this.extensions.runActionById(actionId, context); + trackByActionId(index: number, action: ContentActionRef) { + return action.id; } } diff --git a/src/app/components/preview/preview.component.html b/src/app/components/preview/preview.component.html index f4500aa7a0..fcb9088656 100644 --- a/src/app/components/preview/preview.component.html +++ b/src/app/components/preview/preview.component.html @@ -8,7 +8,7 @@ [canNavigateBefore]="previousNodeId" [canNavigateNext]="nextNodeId" [overlayMode]="true" - (print) = "printFile($event)" + (print)="printFile()" (showViewerChange)="onVisibilityChanged($event)" (navigateBefore)="onNavigateBefore()" (navigateNext)="onNavigateNext()"> @@ -18,74 +18,14 @@ - + + + - - - - - - - - - - - - - - - - - + + diff --git a/src/app/components/preview/preview.component.ts b/src/app/components/preview/preview.component.ts index c007477f0b..25cbe2b0e1 100644 --- a/src/app/components/preview/preview.component.ts +++ b/src/app/components/preview/preview.component.ts @@ -28,7 +28,7 @@ import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_O import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states/app.state'; -import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions'; +import { SetSelectedNodesAction } from '../../store/actions'; import { PageComponent } from '../page.component'; import { ContentApiService } from '../../services/content-api.service'; import { ExtensionService } from '../../extensions/extension.service'; @@ -335,17 +335,7 @@ export class PreviewComponent extends PageComponent implements OnInit { return path; } - deleteFile() { - this.store.dispatch(new DeleteNodesAction([ - { - id: this.node.nodeId || this.node.id, - name: this.node.name - } - ])); - this.onVisibilityChanged(false); - } - - printFile(event: any) { + printFile() { this.viewUtils.printFileGeneric(this.nodeId, this.node.content.mimeType); } diff --git a/src/app/components/preview/preview.module.ts b/src/app/components/preview/preview.module.ts index d718a0bc41..f43f3bcfe4 100644 --- a/src/app/components/preview/preview.module.ts +++ b/src/app/components/preview/preview.module.ts @@ -27,7 +27,7 @@ import { CoreModule } from '@alfresco/adf-core'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; - +import { CoreExtensionsModule } from '../../extensions/core.extensions.module'; import { DirectivesModule } from '../../directives/directives.module'; import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module'; import { PreviewComponent } from './preview.component'; @@ -51,7 +51,8 @@ const routes: Routes = [ CoreModule.forChild(), ContentDirectiveModule, DirectivesModule, - AppInfoDrawerModule + AppInfoDrawerModule, + CoreExtensionsModule.forChild() ], declarations: [ PreviewComponent, diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html index f99f39e672..34090f5921 100644 --- a/src/app/components/recent-files/recent-files.component.html +++ b/src/app/components/recent-files/recent-files.component.html @@ -4,116 +4,10 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
@@ -121,6 +15,7 @@
-
+
diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html index ca29db79e6..760ee310c5 100644 --- a/src/app/components/search/search-results/search-results.component.html +++ b/src/app/components/search/search-results/search-results.component.html @@ -3,80 +3,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + +
@@ -154,7 +82,7 @@
-
+
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html index 1d9de2c106..45e0472372 100644 --- a/src/app/components/shared-files/shared-files.component.html +++ b/src/app/components/shared-files/shared-files.component.html @@ -4,113 +4,10 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -118,6 +15,7 @@
-
+
diff --git a/src/app/components/sidenav/sidenav.component.html b/src/app/components/sidenav/sidenav.component.html index 4a190d3c49..5ce5348922 100644 --- a/src/app/components/sidenav/sidenav.component.html +++ b/src/app/components/sidenav/sidenav.component.html @@ -1,75 +1,39 @@
- - arrow_drop_down + + arrow_drop_down
queue
- - + + - - - - - - -
-
+
    -
  • + [attr.title]="item.description | translate"> + ` +}) +export class DocumentDisplayModeComponent { + + displayMode$: Observable; + + constructor(private store: Store) { + this.displayMode$ = store.select(documentDisplayMode); + } + + onClick() { + this.store.dispatch(new ToggleDocumentDisplayMode()); + } +} diff --git a/src/app/directives/edit-folder.directive.ts b/src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts similarity index 58% rename from src/app/directives/edit-folder.directive.ts rename to src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts index 650e2c3bc0..a7e5279678 100644 --- a/src/app/directives/edit-folder.directive.ts +++ b/src/app/components/toolbar/toggle-favorite/toggle-favorite.component.ts @@ -23,25 +23,30 @@ * along with Alfresco. If not, see . */ -import { Directive, Input, HostListener } from '@angular/core'; -import { MinimalNodeEntity } from 'alfresco-js-api'; +import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states'; -import { EditFolderAction } from '../store/actions'; +import { AppStore, SelectionState } from '../../../store/states'; +import { appSelection } from '../../../store/selectors/app.selectors'; +import { Observable } from 'rxjs/Observable'; -@Directive({ - selector: '[acaEditFolder]' +@Component({ + selector: 'app-toggle-favorite', + template: ` + + ` }) -export class EditFolderDirective { - /** Folder node to edit. */ - // tslint:disable-next-line:no-input-rename - @Input('acaEditFolder') folder: MinimalNodeEntity; +export class ToggleFavoriteComponent { - @HostListener('click', ['$event']) - onClick(event) { - event.preventDefault(); - this.store.dispatch(new EditFolderAction(this.folder)); - } + selection$: Observable; - constructor(private store: Store) {} + constructor(private store: Store) { + this.selection$ = this.store.select(appSelection); + } } diff --git a/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts b/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts new file mode 100644 index 0000000000..8e501a4748 --- /dev/null +++ b/src/app/components/toolbar/toggle-info-drawer/toggle-info-drawer.component.ts @@ -0,0 +1,55 @@ +/*! + * @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 { Component } from '@angular/core'; +import { Observable } from 'rxjs/Rx'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../../store/states'; +import { infoDrawerOpened } from '../../../store/selectors/app.selectors'; +import { ToggleInfoDrawerAction } from '../../../store/actions'; + +@Component({ + selector: 'app-toggle-info-drawer', + template: ` + + ` +}) +export class ToggleInfoDrawerComponent { + infoDrawerOpened$: Observable; + + constructor(private store: Store) { + this.infoDrawerOpened$ = this.store.select(infoDrawerOpened); + } + + onClick() { + this.store.dispatch(new ToggleInfoDrawerAction()); + } +} diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html index 0515ed64d0..eb7a841b10 100644 --- a/src/app/components/trashcan/trashcan.component.html +++ b/src/app/components/trashcan/trashcan.component.html @@ -4,36 +4,10 @@ - + - - - - - - - - - - + +
@@ -41,6 +15,7 @@
{ + entry['isLibrary'] = this.isLibrary; + return entry; + }); + this.store.dispatch( - new SetSelectedNodesAction(this.documentList.selection) + new SetSelectedNodesAction(selection) ); } diff --git a/src/app/directives/node-copy.directive.spec.ts b/src/app/directives/node-copy.directive.spec.ts deleted file mode 100644 index 911c195ce2..0000000000 --- a/src/app/directives/node-copy.directive.spec.ts +++ /dev/null @@ -1,307 +0,0 @@ -/*! - * @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 { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Observable } from 'rxjs/Rx'; -import { MatSnackBar } from '@angular/material'; -import { NodeActionsService } from '../services/node-actions.service'; -import { NodeCopyDirective } from './node-copy.directive'; -import { ContentApiService } from '../services/content-api.service'; -import { AppTestingModule } from '../testing/app-testing.module'; - -@Component({ - template: '
' -}) -class TestComponent { - selection; -} - -describe('NodeCopyDirective', () => { - let fixture: ComponentFixture; - let component: TestComponent; - let element: DebugElement; - let snackBar: MatSnackBar; - let service: NodeActionsService; - let contentApi: ContentApiService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ AppTestingModule ], - declarations: [ - TestComponent, - NodeCopyDirective - ] - }); - - contentApi = TestBed.get(ContentApiService); - - fixture = TestBed.createComponent(TestComponent); - component = fixture.componentInstance; - element = fixture.debugElement.query(By.directive(NodeCopyDirective)); - snackBar = TestBed.get(MatSnackBar); - service = TestBed.get(NodeActionsService); - }); - - describe('Copy node action', () => { - beforeEach(() => { - spyOn(snackBar, 'open').and.callThrough(); - }); - - it('notifies successful copy of a node', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - }); - - it('notifies successful copy of multiple nodes', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [ - { entry: { id: 'node-to-copy-1', name: 'name1' } }, - { entry: { id: 'node-to-copy-2', name: 'name2' } }]; - const createdItems = [ - { entry: { id: 'copy-of-node-1', name: 'name1' } }, - { entry: { id: 'copy-of-node-2', name: 'name2' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL'); - }); - - it('notifies partially copy of one node out of a multiple selection of nodes', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [ - { entry: { id: 'node-to-copy-1', name: 'name1' } }, - { entry: { id: 'node-to-copy-2', name: 'name2' } }]; - const createdItems = [ - { entry: { id: 'copy-of-node-1', name: 'name1' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR'); - }); - - it('notifies partially copy of more nodes out of a multiple selection of nodes', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [ - { entry: { id: 'node-to-copy-0', name: 'name0' } }, - { entry: { id: 'node-to-copy-1', name: 'name1' } }, - { entry: { id: 'node-to-copy-2', name: 'name2' } }]; - const createdItems = [ - { entry: { id: 'copy-of-node-0', name: 'name0' } }, - { entry: { id: 'copy-of-node-1', name: 'name1' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL'); - }); - - it('notifies of failed copy of multiple nodes', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [ - { entry: { id: 'node-to-copy-0', name: 'name0' } }, - { entry: { id: 'node-to-copy-1', name: 'name1' } }, - { entry: { id: 'node-to-copy-2', name: 'name2' } }]; - const createdItems = []; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL'); - }); - - it('notifies of failed copy of one node', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - component.selection = [ - { entry: { id: 'node-to-copy', name: 'name' } }]; - const createdItems = []; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR'); - }); - - it('notifies error if success message was not emitted', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('')); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); - }); - - it('notifies permission error on copy of node', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); - - component.selection = [{ entry: { id: '1', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION'); - }); - - it('notifies generic error message on all errors, but 403', () => { - spyOn(service, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); - - component.selection = [{ entry: { id: '1', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); - }); - }); - - describe('Undo Copy action', () => { - beforeEach(() => { - spyOn(service, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); - - spyOn(snackBar, 'open').and.returnValue({ - onAction: () => Observable.of({}) - }); - }); - - it('should delete the newly created node on Undo action', () => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - - expect(contentApi.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true }); - }); - - it('should delete also the node created inside an already existing folder from destination', () => { - const spyOnDeleteNode = spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); - - component.selection = [ - { entry: { id: 'node-to-copy-1', name: 'name1' } }, - { entry: { id: 'node-to-copy-2', name: 'folder-with-name-already-existing-on-destination' } }]; - const id1 = 'copy-of-node-1'; - const id2 = 'copy-of-child-of-node-2'; - const createdItems = [ - { entry: { id: id1, name: 'name1' } }, - [ { entry: { id: id2, name: 'name-of-child-of-node-2' , parentId: 'the-folder-already-on-destination' } }] ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL'); - - expect(spyOnDeleteNode).toHaveBeenCalled(); - expect(spyOnDeleteNode.calls.allArgs()) - .toEqual([[id1, { permanent: true }], [id2, { permanent: true }]]); - }); - - it('notifies when error occurs on Undo action', () => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(contentApi.deleteNode).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - }); - - it('notifies when some error of type Error occurs on Undo action', () => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error('oops!'))); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(contentApi.deleteNode).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - }); - - it('notifies permission error when it occurs on Undo action', () => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); - - component.selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; - const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentCopied.next(createdItems); - - expect(service.copyNodes).toHaveBeenCalled(); - expect(contentApi.deleteNode).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); - }); - }); - -}); diff --git a/src/app/directives/node-copy.directive.ts b/src/app/directives/node-copy.directive.ts deleted file mode 100644 index c8e994190c..0000000000 --- a/src/app/directives/node-copy.directive.ts +++ /dev/null @@ -1,155 +0,0 @@ -/*! - * @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 { Observable } from 'rxjs/Rx'; -import { MatSnackBar } from '@angular/material'; - -import { TranslationService } from '@alfresco/adf-core'; -import { MinimalNodeEntity } from 'alfresco-js-api'; -import { NodeActionsService } from '../services/node-actions.service'; -import { ContentManagementService } from '../services/content-management.service'; -import { ContentApiService } from '../services/content-api.service'; - -@Directive({ - selector: '[acaCopyNode]' -}) -export class NodeCopyDirective { - - // tslint:disable-next-line:no-input-rename - @Input('acaCopyNode') - selection: MinimalNodeEntity[]; - - @HostListener('click') - onClick() { - this.copySelected(); - } - - constructor( - private content: ContentManagementService, - private contentApi: ContentApiService, - private snackBar: MatSnackBar, - private nodeActionsService: NodeActionsService, - private translation: TranslationService - ) {} - - copySelected() { - Observable.zip( - this.nodeActionsService.copyNodes(this.selection), - this.nodeActionsService.contentCopied - ).subscribe( - (result) => { - const [ operationResult, newItems ] = result; - this.toastMessage(operationResult, newItems); - }, - (error) => { - this.toastMessage(error); - } - ); - } - - private toastMessage(info: any, newItems?: MinimalNodeEntity[]) { - const numberOfCopiedItems = newItems ? newItems.length : 0; - const failedItems = this.selection.length - numberOfCopiedItems; - - let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; - - if (typeof info === 'string') { - if (info.toLowerCase().indexOf('succes') !== -1) { - let i18MessageSuffix; - - if (failedItems) { - if (numberOfCopiedItems) { - i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'PARTIAL_SINGULAR' : 'PARTIAL_PLURAL'; - - } else { - i18MessageSuffix = ( failedItems === 1 ) ? 'FAIL_SINGULAR' : 'FAIL_PLURAL'; - } - - } else { - i18MessageSuffix = ( numberOfCopiedItems === 1 ) ? 'SINGULAR' : 'PLURAL'; - } - - i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`; - } - - } else { - try { - - const { error: { statusCode } } = JSON.parse(info.message); - - if (statusCode === 403) { - i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; - } - - } catch (err) { /* Do nothing, keep the original message */ } - } - - const undo = (numberOfCopiedItems > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : ''; - - const message = this.translation.instant(i18nMessageString, { success: numberOfCopiedItems, failed: failedItems }); - - this.snackBar - .open(message, undo, { - panelClass: 'info-snackbar', - duration: 3000 - }) - .onAction() - .subscribe(() => this.deleteCopy(newItems)); - } - - private deleteCopy(nodes: MinimalNodeEntity[]) { - const batch = this.nodeActionsService.flatten(nodes) - .filter(item => item.entry) - .map(item => this.contentApi.deleteNode(item.entry.id, { permanent: true })); - - Observable.forkJoin(...batch) - .subscribe( - () => { - this.content.nodesDeleted.next(null); - }, - (error) => { - let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; - - let errorJson = null; - try { - errorJson = JSON.parse(error.message); - } catch (e) { // - } - - if (errorJson && errorJson.error && errorJson.error.statusCode === 403) { - i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; - } - - const message = this.translation.instant(i18nMessageString); - - this.snackBar.open(message, '', { - panelClass: 'error-snackbar', - duration: 3000 - }); - } - ); - } -} diff --git a/src/app/directives/node-delete.directive.spec.ts b/src/app/directives/node-delete.directive.spec.ts deleted file mode 100644 index 8fe99c3d8d..0000000000 --- a/src/app/directives/node-delete.directive.spec.ts +++ /dev/null @@ -1,274 +0,0 @@ -/*! - * @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 { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Component, DebugElement } from '@angular/core'; - -import { NodeDeleteDirective } from './node-delete.directive'; -import { EffectsModule, Actions, ofType } from '@ngrx/effects'; -import { NodeEffects } from '../store/effects/node.effects'; -import { - SnackbarInfoAction, SNACKBAR_INFO, SNACKBAR_ERROR, - SnackbarErrorAction, SnackbarWarningAction, SNACKBAR_WARNING -} from '../store/actions'; -import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../testing/app-testing.module'; -import { ContentApiService } from '../services/content-api.service'; -import { Observable } from 'rxjs/Rx'; - -@Component({ - template: '
' -}) -class TestComponent { - selection; -} - -describe('NodeDeleteDirective', () => { - let component: TestComponent; - let fixture: ComponentFixture; - let element: DebugElement; - let actions$: Actions; - let contentApi: ContentApiService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - AppTestingModule, - EffectsModule.forRoot([NodeEffects]) - ], - declarations: [ - NodeDeleteDirective, - TestComponent - ] - }); - - contentApi = TestBed.get(ContentApiService); - actions$ = TestBed.get(Actions); - - fixture = TestBed.createComponent(TestComponent); - component = fixture.componentInstance; - element = fixture.debugElement.query(By.directive(NodeDeleteDirective)); - }); - - describe('Delete action', () => { - it('should raise info message on successful single file deletion', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); - - actions$.pipe( - ofType(SNACKBAR_INFO), - map(action => { - done(); - }) - ); - - component.selection = [{ entry: { id: '1', name: 'name1' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - - it('should raise error message on failed single file deletion', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => { - done(); - }) - ); - - component.selection = [{ entry: { id: '1', name: 'name1' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - - it('should raise info message on successful multiple files deletion', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); - - actions$.pipe( - ofType(SNACKBAR_INFO), - map(action => { - done(); - }) - ); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - - it('should raise error message failed multiple files deletion', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => { - done(); - }) - ); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - - it('should raise warning message when only one file is successful', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.callFake((id) => { - if (id === '1') { - return Observable.throw(null); - } else { - return Observable.of(null); - } - }); - - actions$.pipe( - ofType(SNACKBAR_WARNING), - map(action => { - done(); - }) - ); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - - it('should raise warning message when some files are successfully deleted', fakeAsync(done => { - spyOn(contentApi, 'deleteNode').and.callFake((id) => { - if (id === '1') { - return Observable.throw(null); - } - - if (id === '2') { - return Observable.of(null); - } - - if (id === '3') { - return Observable.of(null); - } - }); - - actions$.pipe( - ofType(SNACKBAR_WARNING), - map(action => { - done(); - }) - ); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } }, - { entry: { id: '3', name: 'name3' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - tick(); - })); - }); - - /* - describe('Restore action', () => { - beforeEach(() => { - spyOn(alfrescoApiService.nodesApi, 'deleteNode').and.returnValue(Promise.resolve(null)); - }); - - it('notifies failed file on on restore', () => { - spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null)); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(spySnackBar.calls.mostRecent().args) - .toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE', '', 3000])); - }); - - it('notifies failed files on on restore', () => { - spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.returnValue(Promise.reject(null)); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(spySnackBar.calls.mostRecent().args) - .toEqual((['APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', '', 3000])); - }); - - it('signals files restored', () => { - spyOn(contentService.nodeRestored, 'next'); - spyOn(alfrescoApiService.nodesApi, 'restoreNode').and.callFake((id) => { - if (id === '1') { - return Promise.resolve(null); - } else { - return Promise.reject(null); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(contentService.nodeRestored.next).toHaveBeenCalled(); - }); - }); - */ -}); diff --git a/src/app/directives/node-delete.directive.ts b/src/app/directives/node-delete.directive.ts deleted file mode 100644 index a5a70a61c2..0000000000 --- a/src/app/directives/node-delete.directive.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * @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 { MinimalNodeEntity } from 'alfresco-js-api'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { DeleteNodesAction } from '../store/actions'; -import { NodeInfo } from '../store/models'; - -@Directive({ - selector: '[acaDeleteNode]' -}) -export class NodeDeleteDirective { - - // tslint:disable-next-line:no-input-rename - @Input('acaDeleteNode') - selection: MinimalNodeEntity[]; - - constructor(private store: Store) {} - - @HostListener('click') - onClick() { - if (this.selection && this.selection.length > 0) { - const toDelete: NodeInfo[] = this.selection.map(node => { - const { name } = node.entry; - const id = node.entry.nodeId || node.entry.id; - - return { - id, - name - }; - }); - this.store.dispatch(new DeleteNodesAction(toDelete)); - } - } -} diff --git a/src/app/directives/node-move.directive.spec.ts b/src/app/directives/node-move.directive.spec.ts deleted file mode 100644 index 9d0ae80028..0000000000 --- a/src/app/directives/node-move.directive.spec.ts +++ /dev/null @@ -1,477 +0,0 @@ -/*! - * @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 { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Observable } from 'rxjs/Rx'; -import { MatSnackBar } from '@angular/material'; -import { TranslationService } from '@alfresco/adf-core'; -import { NodeActionsService } from '../services/node-actions.service'; -import { NodeMoveDirective } from './node-move.directive'; -import { EffectsModule, Actions, ofType } from '@ngrx/effects'; -import { NodeEffects } from '../store/effects/node.effects'; -import { SnackbarErrorAction, SNACKBAR_ERROR } from '../store/actions'; -import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../testing/app-testing.module'; -import { ContentApiService } from '../services/content-api.service'; - -@Component({ - template: '
' -}) -class TestComponent { - selection; -} - -describe('NodeMoveDirective', () => { - let fixture: ComponentFixture; - let component: TestComponent; - let element: DebugElement; - let service: NodeActionsService; - let actions$: Actions; - let translationService: TranslationService; - let contentApi: ContentApiService; - let snackBar: MatSnackBar; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - AppTestingModule, - EffectsModule.forRoot([NodeEffects]) - ], - declarations: [ - NodeMoveDirective, - TestComponent - ] - }); - - contentApi = TestBed.get(ContentApiService); - translationService = TestBed.get(TranslationService); - - actions$ = TestBed.get(Actions); - fixture = TestBed.createComponent(TestComponent); - component = fixture.componentInstance; - element = fixture.debugElement.query(By.directive(NodeMoveDirective)); - service = TestBed.get(NodeActionsService); - snackBar = TestBed.get(MatSnackBar); - }); - - beforeEach(() => { - spyOn(translationService, 'instant').and.callFake((keysArray) => { - if (Array.isArray(keysArray)) { - const processedKeys = {}; - keysArray.forEach((key) => { - processedKeys[key] = key; - }); - return processedKeys; - } else { - return keysArray; - } - }); - }); - - describe('Move node action', () => { - beforeEach(() => { - spyOn(snackBar, 'open').and.callThrough(); - }); - - it('notifies successful move of a node', () => { - const node = [ { entry: { id: 'node-to-move-id', name: 'name' } } ]; - const moveResponse = { - succeeded: node, - failed: [], - partiallySucceeded: [] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = node; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); - }); - - it('notifies successful move of multiple nodes', () => { - const nodes = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } }]; - const moveResponse = { - succeeded: nodes, - failed: [], - partiallySucceeded: [] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = nodes; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL'); - }); - - it('notifies partial move of a node', () => { - const node = [ { entry: { id: '1', name: 'name' } } ]; - const moveResponse = { - succeeded: [], - failed: [], - partiallySucceeded: node - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = node; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR'); - }); - - it('notifies partial move of multiple nodes', () => { - const nodes = [ - { entry: { id: '1', name: 'name' } }, - { entry: { id: '2', name: 'name2' } } ]; - const moveResponse = { - succeeded: [], - failed: [], - partiallySucceeded: nodes - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = nodes; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL'); - }); - - it('notifies successful move and the number of nodes that could not be moved', () => { - const nodes = [ { entry: { id: '1', name: 'name' } }, - { entry: { id: '2', name: 'name2' } } ]; - const moveResponse = { - succeeded: [ nodes[0] ], - failed: [ nodes[1] ], - partiallySucceeded: [] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = nodes; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]) - .toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL'); - }); - - it('notifies successful move and the number of partially moved ones', () => { - const nodes = [ { entry: { id: '1', name: 'name' } }, - { entry: { id: '2', name: 'name2' } } ]; - const moveResponse = { - succeeded: [ nodes[0] ], - failed: [], - partiallySucceeded: [ nodes[1] ] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = nodes; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]) - .toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR'); - }); - - it('notifies error if success message was not emitted', () => { - const node = { entry: { id: 'node-to-move-id', name: 'name' } }; - const moveResponse = { - succeeded: [], - failed: [], - partiallySucceeded: [] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('')); - - component.selection = [ node ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); - }); - - it('notifies permission error on move of node', () => { - spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); - - component.selection = [{ entry: { id: '1', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION'); - }); - - it('notifies generic error message on all errors, but 403', () => { - spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); - - component.selection = [{ entry: { id: '1', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); - }); - - it('notifies conflict error message on 409', () => { - spyOn(service, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}})))); - - component.selection = [{ entry: { id: '1', name: 'name' } }]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE'); - }); - - it('notifies error if move response has only failed items', () => { - const node = [ { entry: { id: '1', name: 'name' } } ]; - const moveResponse = { - succeeded: [], - failed: [ {} ], - partiallySucceeded: [] - }; - - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - spyOn(service, 'processResponse').and.returnValue(moveResponse); - - component.selection = node; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(moveResponse); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); - }); - }); - - describe('Undo Move action', () => { - beforeEach(() => { - spyOn(service, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); - - spyOn(snackBar, 'open').and.returnValue({ - onAction: () => Observable.of({}) - }); - - // spyOn(snackBar, 'open').and.callThrough(); - }); - - it('should move node back to initial parent, after succeeded move', () => { - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; - component.selection = [ node ]; - - spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({})); - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [ { itemMoved: node, initialParentId: initialParent} ] - }; - service.contentMoved.next(movedItems); - - expect(service.moveNodeAction) - .toHaveBeenCalledWith(movedItems.succeeded[0].itemMoved.entry, movedItems.succeeded[0].initialParentId); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); - }); - - it('should move node back to initial parent, after succeeded move of a single file', () => { - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'node-to-move-id', name: 'name', isFolder: false, parentId: initialParent } }; - component.selection = [ node ]; - - spyOn(service, 'moveNodeAction').and.returnValue(Observable.of({})); - - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [ node ] - }; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(movedItems); - - expect(service.moveNodeAction).toHaveBeenCalledWith(node.entry, initialParent); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); - }); - - it('should restore deleted folder back to initial parent, after succeeded moving all its files', () => { - // when folder was deleted after all its children were moved to a folder with the same name from destination - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of(null)); - - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'folder-to-move-id', name: 'conflicting-name', parentId: initialParent, isFolder: true } }; - component.selection = [ node ]; - - const itemMoved = {}; // folder was empty - service.moveDeletedEntries = [ node ]; // folder got deleted - - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [ [ itemMoved ] ] - }; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(movedItems); - - expect(contentApi.restoreNode).toHaveBeenCalled(); - expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); - }); - - it('should notify when error occurs on Undo Move action', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(null)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'node-to-move-id', name: 'conflicting-name', parentId: initialParent } }; - component.selection = [node]; - - const afterMoveParentId = 'parent-id-1'; - const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name', parentId: afterMoveParentId } }; - service.moveDeletedEntries = [ node ]; // folder got deleted - - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] - }; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(movedItems); - - expect(contentApi.restoreNode).toHaveBeenCalled(); - })); - - it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error('oops!'))); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; - component.selection = [ node ]; - - const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; - service.moveDeletedEntries = [ node ]; // folder got deleted - - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] - }; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(movedItems); - - expect(contentApi.restoreNode).toHaveBeenCalled(); - })); - - it('should notify permission error when it occurs on Undo Move action', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const initialParent = 'parent-id-0'; - const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; - component.selection = [ node ]; - - const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; - service.moveDeletedEntries = [ node ]; // folder got deleted - - const movedItems = { - failed: [], - partiallySucceeded: [], - succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] - }; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - service.contentMoved.next(movedItems); - - expect(service.moveNodes).toHaveBeenCalled(); - expect(contentApi.restoreNode).toHaveBeenCalled(); - })); - }); - -}); diff --git a/src/app/directives/node-move.directive.ts b/src/app/directives/node-move.directive.ts deleted file mode 100644 index 09aa9105e2..0000000000 --- a/src/app/directives/node-move.directive.ts +++ /dev/null @@ -1,223 +0,0 @@ -/*! - * @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 { TranslationService } from '@alfresco/adf-core'; -import { MinimalNodeEntity } from 'alfresco-js-api'; -import { MatSnackBar } from '@angular/material'; - -import { ContentManagementService } from '../services/content-management.service'; -import { NodeActionsService } from '../services/node-actions.service'; -import { Observable } from 'rxjs/Rx'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { SnackbarErrorAction } from '../store/actions'; -import { ContentApiService } from '../services/content-api.service'; - -@Directive({ - selector: '[acaMoveNode]' -}) - -export class NodeMoveDirective { - // tslint:disable-next-line:no-input-rename - @Input('acaMoveNode') - selection: MinimalNodeEntity[]; - - @HostListener('click') - onClick() { - this.moveSelected(); - } - - constructor( - private store: Store, - private contentApi: ContentApiService, - private content: ContentManagementService, - private nodeActionsService: NodeActionsService, - private translation: TranslationService, - private snackBar: MatSnackBar - ) {} - - moveSelected() { - const permissionForMove = '!'; - - Observable.zip( - this.nodeActionsService.moveNodes(this.selection, permissionForMove), - this.nodeActionsService.contentMoved - ).subscribe( - (result) => { - const [ operationResult, moveResponse ] = result; - this.toastMessage(operationResult, moveResponse); - - this.content.nodesMoved.next(null); - }, - (error) => { - this.toastMessage(error); - } - ); - } - - private toastMessage(info: any, moveResponse?: any) { - const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0; - const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0; - const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0; - - let successMessage = ''; - let partialSuccessMessage = ''; - let failedMessage = ''; - let errorMessage = ''; - - if (typeof info === 'string') { - - // in case of success - if (info.toLowerCase().indexOf('succes') !== -1) { - const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.'; - let i18MessageSuffix = ''; - - if (succeeded) { - i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL'; - successMessage = `${i18nMessageString}${i18MessageSuffix}`; - } - - if (partiallySucceeded) { - i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL'; - partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`; - } - - if (failures) { - // if moving failed for ALL nodes, emit error - if (failures === this.selection.length) { - const errors = this.nodeActionsService.flatten(moveResponse['failed']); - errorMessage = this.getErrorMessage(errors[0]); - - } else { - i18MessageSuffix = 'PARTIAL.FAIL'; - failedMessage = `${i18nMessageString}${i18MessageSuffix}`; - } - } - } else { - errorMessage = 'APP.MESSAGES.ERRORS.GENERIC'; - } - - } else { - errorMessage = this.getErrorMessage(info); - } - - const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : ''; - failedMessage = errorMessage ? errorMessage : failedMessage; - - const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : ''; - const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : ''; - - const initialParentId = this.nodeActionsService.getEntryParentId(this.selection[0].entry); - - const messages = this.translation.instant( - [successMessage, partialSuccessMessage, failedMessage], - { success: succeeded, failed: failures, partially: partiallySucceeded} - ); - - // TODO: review in terms of i18n - this.snackBar - .open( - messages[successMessage] - + beforePartialSuccessMessage + messages[partialSuccessMessage] - + beforeFailedMessage + messages[failedMessage] - , undo, { - panelClass: 'info-snackbar', - duration: 3000 - }) - .onAction() - .subscribe(() => this.revertMoving(moveResponse, initialParentId)); - } - - getErrorMessage(errorObject): string { - let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; - - try { - const { error: { statusCode } } = JSON.parse(errorObject.message); - - if (statusCode === 409) { - i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE'; - - } else if (statusCode === 403) { - i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; - } - - } catch (err) { /* Do nothing, keep the original message */ } - - return i18nMessageString; - } - - private revertMoving(moveResponse, selectionParentId) { - const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : []; - const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : []; - - const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries - .map((folderEntry) => { - return this.contentApi - .restoreNode(folderEntry.nodeId || folderEntry.id) - .map(node => node.entry); - }); - - Observable.zip(...restoreDeletedNodesBatch, Observable.of(null)) - .flatMap(() => { - - const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes]; - - const revertMoveBatch = this.nodeActionsService - .flatten(nodesToBeMovedBack) - .filter(node => node.entry || (node.itemMoved && node.itemMoved.entry)) - .map((node) => { - if (node.itemMoved) { - return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId); - } else { - return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId); - } - }); - - return Observable.zip(...revertMoveBatch, Observable.of(null)); - }) - .subscribe( - () => { - this.content.nodesMoved.next(null); - }, - error => { - let message = 'APP.MESSAGES.ERRORS.GENERIC'; - - let errorJson = null; - try { - errorJson = JSON.parse(error.message); - } catch {} - - if (errorJson && errorJson.error && errorJson.error.statusCode === 403) { - message = 'APP.MESSAGES.ERRORS.PERMISSION'; - } - - this.store.dispatch(new SnackbarErrorAction(message)); - } - ); - } - -} diff --git a/src/app/directives/node-permanent-delete.directive.spec.ts b/src/app/directives/node-permanent-delete.directive.spec.ts deleted file mode 100644 index 6981144eb8..0000000000 --- a/src/app/directives/node-permanent-delete.directive.spec.ts +++ /dev/null @@ -1,272 +0,0 @@ -/*! - * @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 { Component, DebugElement } from '@angular/core'; -import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Observable } from 'rxjs/Rx'; - -import { NodePermanentDeleteDirective } from './node-permanent-delete.directive'; -import { MatDialog } from '@angular/material'; -import { Actions, ofType, EffectsModule } from '@ngrx/effects'; -import { - SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction, - SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING -} from '../store/actions'; -import { map } from 'rxjs/operators'; -import { NodeEffects } from '../store/effects/node.effects'; -import { AppTestingModule } from '../testing/app-testing.module'; -import { ContentApiService } from '../services/content-api.service'; - -@Component({ - template: `
` -}) -class TestComponent { - selection = []; -} - -describe('NodePermanentDeleteDirective', () => { - let fixture: ComponentFixture; - let element: DebugElement; - let component: TestComponent; - let dialog: MatDialog; - let actions$: Actions; - let contentApi: ContentApiService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - AppTestingModule, - EffectsModule.forRoot([NodeEffects]) - ], - declarations: [ - NodePermanentDeleteDirective, - TestComponent - ] - }); - - contentApi = TestBed.get(ContentApiService); - actions$ = TestBed.get(Actions); - - fixture = TestBed.createComponent(TestComponent); - component = fixture.componentInstance; - element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective)); - - dialog = TestBed.get(MatDialog); - spyOn(dialog, 'open').and.returnValue({ - afterClosed() { - return Observable.of(true); - } - }); - }); - - it('does not purge nodes if no selection', () => { - spyOn(contentApi, 'purgeDeletedNode'); - - component.selection = []; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(contentApi.purgeDeletedNode).not.toHaveBeenCalled(); - }); - - it('call purge nodes if selection is not empty', fakeAsync(() => { - spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); - - component.selection = [ { entry: { id: '1' } } ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(contentApi.purgeDeletedNode).toHaveBeenCalled(); - })); - - describe('notification', () => { - it('raises warning on multiple fail and one success', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_WARNING), - map((action: SnackbarWarningAction) => { - done(); - }) - ); - - spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { - if (id === '1') { - return Observable.of({}); - } - - if (id === '2') { - return Observable.throw({}); - } - - if (id === '3') { - return Observable.throw({}); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } }, - { entry: { id: '3', name: 'name3' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('raises warning on multiple success and multiple fail', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_WARNING), - map((action: SnackbarWarningAction) => { - done(); - }) - ); - - spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { - if (id === '1') { - return Observable.of({}); - } - - if (id === '2') { - return Observable.throw({}); - } - - if (id === '3') { - return Observable.throw({}); - } - - if (id === '4') { - return Observable.of({}); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } }, - { entry: { id: '3', name: 'name3' } }, - { entry: { id: '4', name: 'name4' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('raises info on one selected node success', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_INFO), - map((action: SnackbarInfoAction) => { - done(); - }) - ); - - spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('raises error on one selected node fail', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_ERROR), - map((action: SnackbarErrorAction) => { - done(); - }) - ); - - spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.throw({})); - - component.selection = [ - { entry: { id: '1', name: 'name1' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('raises info on all nodes success', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_INFO), - map((action: SnackbarInfoAction) => { - done(); - }) - ); - spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { - if (id === '1') { - return Observable.of({}); - } - - if (id === '2') { - return Observable.of({}); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('raises error on all nodes fail', fakeAsync(done => { - actions$.pipe( - ofType(SNACKBAR_ERROR), - map((action: SnackbarErrorAction) => { - done(); - }) - ); - spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { - if (id === '1') { - return Observable.throw({}); - } - - if (id === '2') { - return Observable.throw({}); - } - }); - - component.selection = [ - { entry: { id: '1', name: 'name1' } }, - { entry: { id: '2', name: 'name2' } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - }); -}); diff --git a/src/app/directives/node-permissions.directive.ts b/src/app/directives/node-permissions.directive.ts deleted file mode 100644 index 92973e1043..0000000000 --- a/src/app/directives/node-permissions.directive.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*! - * @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 { MinimalNodeEntity } from 'alfresco-js-api'; -import { MatDialog } from '@angular/material'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { SnackbarErrorAction } from '../store/actions'; -import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog'; - -@Directive({ - selector: '[acaNodePermissions]' -}) -export class NodePermissionsDirective { - // tslint:disable-next-line:no-input-rename - @Input('acaNodePermissions') node: MinimalNodeEntity; - - @HostListener('click') - onClick() { - this.showPermissions(); - } - - constructor( - private store: Store, - private dialog: MatDialog - ) {} - - showPermissions() { - if (this.node) { - let entry; - if (this.node.entry) { - entry = this.node.entry; - - } else { - entry = this.node; - } - - const entryId = entry.nodeId || (entry).guid || entry.id; - this.openPermissionsDialog(entryId); - } - } - - openPermissionsDialog(nodeId: string) { - // workaround Shared - if (nodeId) { - this.dialog.open(NodePermissionsDialogComponent, { - data: { nodeId }, - panelClass: 'aca-permissions-dialog-panel', - width: '730px' - }); - } else { - this.store.dispatch( - new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION') - ); - } - } -} diff --git a/src/app/directives/node-restore.directive.spec.ts b/src/app/directives/node-restore.directive.spec.ts deleted file mode 100644 index 02d85912b6..0000000000 --- a/src/app/directives/node-restore.directive.spec.ts +++ /dev/null @@ -1,390 +0,0 @@ -/*! - * @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 { Component, DebugElement } from '@angular/core'; -import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NodeRestoreDirective } from './node-restore.directive'; -import { ContentManagementService } from '../services/content-management.service'; -import { Actions, ofType, EffectsModule } from '@ngrx/effects'; -import { SnackbarErrorAction, - SNACKBAR_ERROR, SnackbarInfoAction, SNACKBAR_INFO, - NavigateRouteAction, NAVIGATE_ROUTE } from '../store/actions'; -import { map } from 'rxjs/operators'; -import { AppTestingModule } from '../testing/app-testing.module'; -import { ContentApiService } from '../services/content-api.service'; -import { Observable } from 'rxjs/Rx'; -import { NodeEffects } from '../store/effects'; -import { MinimalNodeEntity } from 'alfresco-js-api'; - -@Component({ - template: `
` -}) -class TestComponent { - selection: Array = []; -} - -describe('NodeRestoreDirective', () => { - let fixture: ComponentFixture; - let element: DebugElement; - let component: TestComponent; - let contentManagementService: ContentManagementService; - let actions$: Actions; - let contentApi: ContentApiService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - AppTestingModule, - EffectsModule.forRoot([NodeEffects]) - ], - declarations: [ - NodeRestoreDirective, - TestComponent - ] - }); - - actions$ = TestBed.get(Actions); - - fixture = TestBed.createComponent(TestComponent); - component = fixture.componentInstance; - element = fixture.debugElement.query(By.directive(NodeRestoreDirective)); - - contentManagementService = TestBed.get(ContentManagementService); - contentApi = TestBed.get(ContentApiService); - }); - - it('does not restore nodes if no selection', () => { - spyOn(contentApi, 'restoreNode'); - - component.selection = []; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(contentApi.restoreNode).not.toHaveBeenCalled(); - }); - - it('does not restore nodes if selection has nodes without path', () => { - spyOn(contentApi, 'restoreNode'); - - component.selection = [ { entry: { id: '1' } } ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - - expect(contentApi.restoreNode).not.toHaveBeenCalled(); - }); - - it('call restore nodes if selection has nodes with path', fakeAsync(() => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); - spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ - list: { entries: [] } - })); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { - entry: { - id: '1', - path - } - } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - expect(contentApi.restoreNode).toHaveBeenCalled(); - })); - - describe('refresh()', () => { - it('dispatch event on finish', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); - spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ - list: { entries: [] } - })); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { - entry: { - id: '1', - path - } - } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - - contentManagementService.nodesRestored.subscribe(() => done()); - })); - }); - - describe('notification', () => { - beforeEach(() => { - spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ - list: { entries: [] } - })); - }); - - it('should raise error message on partial multiple fail ', fakeAsync(done => { - const error = { message: '{ "error": {} }' }; - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - spyOn(contentApi, 'restoreNode').and.callFake((id) => { - if (id === '1') { - return Observable.of({}); - } - - if (id === '2') { - return Observable.throw(error); - } - - if (id === '3') { - return Observable.throw(error); - } - }); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } }, - { entry: { id: '2', name: 'name2', path } }, - { entry: { id: '3', name: 'name3', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('should raise error message when restored node exist, error 409', fakeAsync(done => { - const error = { message: '{ "error": { "statusCode": 409 } }' }; - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('should raise error message when restored node returns different statusCode', fakeAsync(done => { - const error = { message: '{ "error": { "statusCode": 404 } }' }; - - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('should raise error message when restored node location is missing', fakeAsync(done => { - const error = { message: '{ "error": { } }' }; - - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); - - actions$.pipe( - ofType(SNACKBAR_ERROR), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('should raise info message when restore multiple nodes', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.callFake((id) => { - if (id === '1') { - return Observable.of({}); - } - - if (id === '2') { - return Observable.of({}); - } - }); - - actions$.pipe( - ofType(SNACKBAR_INFO), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } }, - { entry: { id: '2', name: 'name2', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - xit('should raise info message when restore selected node', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); - - actions$.pipe( - ofType(SNACKBAR_INFO), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { entry: { id: '1', name: 'name1', path } } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - - it('navigate to restore selected node location onAction', fakeAsync(done => { - spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); - - actions$.pipe( - ofType(NAVIGATE_ROUTE), - map(action => done()) - ); - - const path = { - elements: [ - { - id: '1-1', - name: 'somewhere-over-the-rainbow' - } - ] - }; - - component.selection = [ - { - entry: { - id: '1', - name: 'name1', - path - } - } - ]; - - fixture.detectChanges(); - element.triggerEventHandler('click', null); - tick(); - })); - }); -}); diff --git a/src/app/directives/node-unshare.directive.ts b/src/app/directives/node-unshare.directive.ts deleted file mode 100644 index 2426e393ee..0000000000 --- a/src/app/directives/node-unshare.directive.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * @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 { MinimalNodeEntity } from 'alfresco-js-api'; -import { ContentManagementService } from '../services/content-management.service'; -import { ContentApiService } from '../services/content-api.service'; - -@Directive({ - selector: '[acaUnshareNode]' -}) -export class NodeUnshareDirective { - - // tslint:disable-next-line:no-input-rename - @Input('acaUnshareNode') - selection: MinimalNodeEntity[]; - - constructor( - private contentApi: ContentApiService, - private contentManagement: ContentManagementService) { - } - - @HostListener('click') - onClick() { - if (this.selection.length > 0) { - this.unshareLinks(this.selection); - } - } - - private async unshareLinks(links: MinimalNodeEntity[]) { - const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise()); - await Promise.all(promises); - this.contentManagement.linksUnshared.next(); - } -} diff --git a/src/app/directives/node-versions.directive.ts b/src/app/directives/node-versions.directive.ts deleted file mode 100644 index aceddc086d..0000000000 --- a/src/app/directives/node-versions.directive.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*! - * @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 { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog'; -import { MatDialog } from '@angular/material'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states/app.state'; -import { SnackbarErrorAction } from '../store/actions'; -import { ContentApiService } from '../services/content-api.service'; - -@Directive({ - selector: '[acaNodeVersions]' -}) -export class NodeVersionsDirective { - // tslint:disable-next-line:no-input-rename - @Input('acaNodeVersions') node: MinimalNodeEntity; - - @HostListener('click') - onClick() { - this.onManageVersions(); - } - - constructor( - private store: Store, - private contentApi: ContentApiService, - private dialog: MatDialog - ) {} - - async onManageVersions() { - if (this.node && this.node.entry) { - let entry = this.node.entry; - - if (entry.nodeId || (entry).guid) { - entry = await this.contentApi.getNodeInfo( - entry.nodeId || (entry).id - ).toPromise(); - this.openVersionManagerDialog(entry); - } else { - this.openVersionManagerDialog(entry); - } - } else if (this.node) { - this.openVersionManagerDialog(this.node); - } - } - - openVersionManagerDialog(node: MinimalNodeEntryEntity) { - // workaround Shared - if (node.isFile || node.nodeId) { - this.dialog.open(NodeVersionsDialogComponent, { - data: { node }, - panelClass: 'adf-version-manager-dialog-panel', - width: '630px' - }); - } else { - this.store.dispatch( - new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION') - ); - } - } -} diff --git a/src/app/extensions/action.extensions.ts b/src/app/extensions/action.extensions.ts index 7135fb22e7..24f2ddb574 100644 --- a/src/app/extensions/action.extensions.ts +++ b/src/app/extensions/action.extensions.ts @@ -24,7 +24,7 @@ */ export enum ContentActionType { - default = 'button', + default = 'default', button = 'button', separator = 'separator', menu = 'menu', @@ -36,6 +36,7 @@ export interface ContentActionRef { type: ContentActionType; title?: string; + description?: string; order?: number; icon?: string; disabled?: boolean; diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html deleted file mode 100644 index b69d1f8d41..0000000000 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/extensions/components/toolbar/toolbar-action.component.html b/src/app/extensions/components/toolbar/toolbar-action.component.html new file mode 100644 index 0000000000..9a6f36050c --- /dev/null +++ b/src/app/extensions/components/toolbar/toolbar-action.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts b/src/app/extensions/components/toolbar/toolbar-action.component.ts similarity index 66% rename from src/app/extensions/components/toolbar-action/toolbar-action.component.ts rename to src/app/extensions/components/toolbar/toolbar-action.component.ts index d3b18c5496..c2d3c64837 100644 --- a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts +++ b/src/app/extensions/components/toolbar/toolbar-action.component.ts @@ -27,16 +27,11 @@ import { Component, ViewEncapsulation, ChangeDetectionStrategy, - Input, - OnInit, - OnDestroy + Input } from '@angular/core'; -import { AppStore, SelectionState } from '../../../store/states'; +import { AppStore } from '../../../store/states'; import { Store } from '@ngrx/store'; import { ExtensionService } from '../../extension.service'; -import { appSelection } from '../../../store/selectors/app.selectors'; -import { Subject } from 'rxjs/Rx'; -import { takeUntil } from 'rxjs/operators'; import { ContentActionRef } from '../../action.extensions'; @Component({ @@ -46,36 +41,16 @@ import { ContentActionRef } from '../../action.extensions'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'aca-toolbar-action' } }) -export class ToolbarActionComponent implements OnInit, OnDestroy { +export class ToolbarActionComponent { + @Input() type = 'icon-button'; @Input() entry: ContentActionRef; - selection: SelectionState; - onDestroy$: Subject = new Subject(); - constructor( protected store: Store, protected extensions: ExtensionService ) {} - ngOnInit() { - this.store - .select(appSelection) - .pipe(takeUntil(this.onDestroy$)) - .subscribe(selection => { - this.selection = selection; - }); - } - - ngOnDestroy() { - this.onDestroy$.next(true); - this.onDestroy$.complete(); - } - - runAction(actionId: string) { - const context = { - selection: this.selection - }; - - this.extensions.runActionById(actionId, context); + trackByActionId(index: number, action: ContentActionRef) { + return action.id; } } diff --git a/src/app/extensions/components/toolbar/toolbar-button.component.ts b/src/app/extensions/components/toolbar/toolbar-button.component.ts new file mode 100644 index 0000000000..122a4a3fe5 --- /dev/null +++ b/src/app/extensions/components/toolbar/toolbar-button.component.ts @@ -0,0 +1,90 @@ +/*! + * @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 { Component, Input } from '@angular/core'; +import { ContentActionRef } from '../../action.extensions'; +import { ExtensionService } from '../../extension.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../../store/states'; +import { appSelection } from '../../../store/selectors/app.selectors'; + +export enum ToolbarButtonType { + ICON_BUTTON = 'icon-button', + MENU_ITEM = 'menu-item' +} + +@Component({ + selector: 'app-toolbar-button', + template: ` + + + + + + + + + ` +}) +export class ToolbarButtonComponent { + @Input() type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON; + @Input() actionRef: ContentActionRef; + + constructor( + protected store: Store, + private extensions: ExtensionService + ) {} + + runAction() { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + this.extensions.runActionById(this.actionRef.actions.click, { + selection + }); + }); + } +} diff --git a/src/app/extensions/core.extensions.module.ts b/src/app/extensions/core.extensions.module.ts index 197e841d6a..37efa528ca 100644 --- a/src/app/extensions/core.extensions.module.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -28,11 +28,14 @@ import { CommonModule } from '@angular/common'; import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; import { LayoutComponent } from '../components/layout/layout.component'; import { TrashcanComponent } from '../components/trashcan/trashcan.component'; -import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component'; +import { ToolbarActionComponent } from './components/toolbar/toolbar-action.component'; import * as app from './evaluators/app.evaluators'; +import * as nav from './evaluators/navigation.evaluators'; import { ExtensionService } from './extension.service'; import { CustomExtensionComponent } from './components/custom-component/custom.component'; -import { DemoButtonComponent } from './components/custom-component/demo.button'; +import { ToggleInfoDrawerComponent } from '../components/toolbar/toggle-info-drawer/toggle-info-drawer.component'; +import { ToggleFavoriteComponent } from '../components/toolbar/toggle-favorite/toggle-favorite.component'; +import { ToolbarButtonComponent } from './components/toolbar/toolbar-button.component'; export function setupExtensions(extensions: ExtensionService): Function { return () => @@ -40,7 +43,8 @@ export function setupExtensions(extensions: ExtensionService): Function { extensions.setComponents({ 'app.layout.main': LayoutComponent, 'app.components.trashcan': TrashcanComponent, - 'app.demo.button': DemoButtonComponent + 'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent, + 'app.toolbar.toggleFavorite': ToggleFavoriteComponent }); extensions.setAuthGuards({ @@ -48,14 +52,33 @@ export function setupExtensions(extensions: ExtensionService): Function { }); extensions.setEvaluators({ + 'app.selection.canDelete': app.canDeleteSelection, 'app.selection.canDownload': app.canDownloadSelection, 'app.selection.notEmpty': app.hasSelection, + 'app.selection.canUnshare': app.canUnshareNodes, + 'app.selection.canAddFavorite': app.canAddFavorite, + 'app.selection.canRemoveFavorite': app.canRemoveFavorite, + 'app.selection.first.canUpdate': app.canUpdateSelectedNode, 'app.selection.file': app.hasFileSelected, + 'app.selection.file.canShare': app.canShareFile, + 'app.selection.library': app.hasLibrarySelected, 'app.selection.folder': app.hasFolderSelected, 'app.selection.folder.canUpdate': app.canUpdateSelectedFolder, + 'app.navigation.folder.canCreate': app.canCreateFolder, - 'app.navigation.isTrashcan': app.isTrashcan, - 'app.navigation.isNotTrashcan': app.isNotTrashcan + 'app.navigation.folder.canUpload': app.canUpload, + 'app.navigation.isTrashcan': nav.isTrashcan, + 'app.navigation.isNotTrashcan': nav.isNotTrashcan, + 'app.navigation.isLibraries': nav.isLibraries, + 'app.navigation.isNotLibraries': nav.isNotLibraries, + 'app.navigation.isSharedFiles': nav.isSharedFiles, + 'app.navigation.isNotSharedFiles': nav.isNotSharedFiles, + 'app.navigation.isFavorites': nav.isFavorites, + 'app.navigation.isNotFavorites': nav.isNotFavorites, + 'app.navigation.isRecentFiles': nav.isRecentFiles, + 'app.navigation.isNotRecentFiles': nav.isNotRecentFiles, + 'app.navigation.isSearchResults': nav.isSearchResults, + 'app.navigation.isNotSearchResults': nav.isNotSearchResults }); resolve(true); @@ -66,15 +89,13 @@ export function setupExtensions(extensions: ExtensionService): Function { imports: [CommonModule, CoreModule.forChild()], declarations: [ ToolbarActionComponent, - CustomExtensionComponent, - DemoButtonComponent + ToolbarButtonComponent, + CustomExtensionComponent ], exports: [ ToolbarActionComponent, + ToolbarButtonComponent, CustomExtensionComponent - ], - entryComponents: [ - DemoButtonComponent ] }) export class CoreExtensionsModule { diff --git a/src/app/extensions/evaluators/app.evaluators.ts b/src/app/extensions/evaluators/app.evaluators.ts index 79b5959cab..d14cae9e1d 100644 --- a/src/app/extensions/evaluators/app.evaluators.ts +++ b/src/app/extensions/evaluators/app.evaluators.ts @@ -23,63 +23,201 @@ * along with Alfresco. If not, see . */ -import { Node } from 'alfresco-js-api'; import { RuleContext, RuleParameter } from '../rule.extensions'; +import { + isNotTrashcan, + isNotSharedFiles, + isNotLibraries, + isFavorites, + isLibraries, + isTrashcan, + isSharedFiles, + isNotSearchResults +} from './navigation.evaluators'; -export function isTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean { - const { url } = context.navigation; - return url && url.startsWith('/trashcan'); +export function canAddFavorite( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if (!context.selection.isEmpty) { + if ( + isFavorites(context, ...args) || + isLibraries(context, ...args) || + isTrashcan(context, ...args) + ) { + return false; + } + return context.selection.nodes.some(node => !node.entry.isFavorite); + } + return false; } -export function isNotTrashcan(context: RuleContext, ...args: RuleParameter[]): boolean { - return !isTrashcan(context, ...args); +export function canRemoveFavorite( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if (!context.selection.isEmpty && !isTrashcan(context, ...args)) { + if (isFavorites(context, ...args)) { + return true; + } + return context.selection.nodes.every(node => node.entry.isFavorite); + } + return false; } -export function hasSelection(context: RuleContext, ...args: RuleParameter[]): boolean { - const { selection } = context; - return selection && !selection.isEmpty; +export function canShareFile( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if ( + isNotTrashcan(context, ...args) && + isNotSharedFiles(context, ...args) && + context.selection.file + ) { + return true; + } + return false; } -export function canCreateFolder(context: RuleContext, ...args: RuleParameter[]): boolean { - const folder = context.navigation.currentFolder; - if (folder) { - return nodeHasPermission(folder, 'create'); +export function canDeleteSelection( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if ( + isNotTrashcan(context, ...args) && + isNotLibraries(context, ...args) && + isNotSearchResults(context, ...args) && + !context.selection.isEmpty + ) { + // temp workaround for Search api + if (isFavorites(context, ...args)) { + return true; + } + + // workaround for Shared Files + if (isSharedFiles(context, ...args)) { + return context.permissions.check( + context.selection.nodes, + ['delete'], + { target: 'allowableOperationsOnTarget' }); + } + + return context.permissions.check(context.selection.nodes, ['delete']); } return false; } -export function canDownloadSelection(context: RuleContext, ...args: RuleParameter[]): boolean { +export function canUnshareNodes( + context: RuleContext, + ...args: RuleParameter[] +): boolean { if (!context.selection.isEmpty) { - return context.selection.nodes.every(node => { - return node.entry && (node.entry.isFile || node.entry.isFolder || !!node.entry.nodeId); + return context.permissions.check(context.selection.nodes, ['delete'], { + target: 'allowableOperationsOnTarget' }); } return false; +} + +export function hasSelection( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !context.selection.isEmpty; +} +export function canCreateFolder( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { currentFolder } = context.navigation; + if (currentFolder) { + return context.permissions.check(currentFolder, ['create']); + } + return false; } -export function hasFolderSelected(context: RuleContext, ...args: RuleParameter[]): boolean { +export function canUpload( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { currentFolder } = context.navigation; + if (currentFolder) { + return context.permissions.check(currentFolder, ['create']); + } + return false; +} + +export function canDownloadSelection( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if (!context.selection.isEmpty) { + return context.selection.nodes.every(node => { + return ( + node.entry && + (node.entry.isFile || + node.entry.isFolder || + !!node.entry.nodeId) + ); + }); + } + return false; +} + +export function hasFolderSelected( + context: RuleContext, + ...args: RuleParameter[] +): boolean { const folder = context.selection.folder; return folder ? true : false; } -export function hasFileSelected(context: RuleContext, ...args: RuleParameter[]): boolean { +export function hasLibrarySelected( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const library = context.selection.library; + return library ? true : false; +} + +export function hasFileSelected( + context: RuleContext, + ...args: RuleParameter[] +): boolean { const file = context.selection.file; return file ? true : false; } -export function canUpdateSelectedFolder(context: RuleContext, ...args: RuleParameter[]): boolean { - const folder = context.selection.folder; - if (folder && folder.entry) { - return nodeHasPermission(folder.entry, 'update'); +export function canUpdateSelectedNode( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + if (context.selection && !context.selection.isEmpty) { + const node = context.selection.first; + + if (node.entry.hasOwnProperty('allowableOperationsOnTarget')) { + return context.permissions.check(node, ['update'], { + target: 'allowableOperationsOnTarget' + }); + } + + return context.permissions.check(node, ['update']); } return false; } -export function nodeHasPermission(node: Node, permission: string): boolean { - if (node && permission) { - const allowableOperations = node.allowableOperations || []; - return allowableOperations.includes(permission); +export function canUpdateSelectedFolder( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { folder } = context.selection; + if (folder) { + return ( + // workaround for Search Api + isFavorites(context, ...args) || + context.permissions.check(folder.entry, ['update']) + ); } return false; } diff --git a/src/app/extensions/evaluators/navigation.evaluators.ts b/src/app/extensions/evaluators/navigation.evaluators.ts new file mode 100644 index 0000000000..f6970f5a6a --- /dev/null +++ b/src/app/extensions/evaluators/navigation.evaluators.ts @@ -0,0 +1,116 @@ +/*! + * @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 { RuleContext, RuleParameter } from '../rule.extensions'; + +export function isFavorites( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/favorites'); +} + +export function isNotFavorites( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isFavorites(context, ...args); +} + +export function isSharedFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/shared'); +} + +export function isNotSharedFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isSharedFiles(context, ...args); +} + +export function isTrashcan( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/trashcan'); +} + +export function isNotTrashcan( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isTrashcan(context, ...args); +} + +export function isLibraries( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.endsWith('/libraries'); +} + +export function isNotLibraries( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isLibraries(context, ...args); +} + +export function isRecentFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/recent-files'); +} + +export function isNotRecentFiles( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isRecentFiles(context, ...args); +} + +export function isSearchResults( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + const { url } = context.navigation; + return url && url.startsWith('/search'); +} + +export function isNotSearchResults( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !isSearchResults(context, ...args); +} diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index b284a86d02..2ba72f9e36 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -41,6 +41,7 @@ export interface ExtensionConfig { create?: Array; viewer?: { openWith?: Array; + actions?: Array; }; navbar?: Array; content?: { diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 06b9a6d70a..81f6772b8e 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -36,6 +36,7 @@ import { RouteRef } from './routing.extensions'; import { RuleContext, RuleRef, RuleEvaluator } from './rule.extensions'; import { ActionRef, ContentActionRef, ContentActionType } from './action.extensions'; import * as core from './evaluators/core.evaluators'; +import { NodePermissionService } from '../services/node-permission.service'; @Injectable() export class ExtensionService implements RuleContext { @@ -52,6 +53,7 @@ export class ExtensionService implements RuleContext { actions: Array = []; contentActions: Array = []; + viewerActions: Array = []; openWithActions: Array = []; createActions: Array = []; navbar: Array = []; @@ -63,7 +65,10 @@ export class ExtensionService implements RuleContext { selection: SelectionState; navigation: NavigationState; - constructor(private http: HttpClient, private store: Store) { + constructor( + private http: HttpClient, + private store: Store, + public permissions: NodePermissionService) { this.evaluators = { 'core.every': core.every, @@ -118,6 +123,7 @@ export class ExtensionService implements RuleContext { this.actions = this.loadActions(config); this.routes = this.loadRoutes(config); this.contentActions = this.loadContentActions(config); + this.viewerActions = this.loadViewerActions(config); this.openWithActions = this.loadViewerOpenWith(config); this.createActions = this.loadCreateActions(config); this.navbar = this.loadNavBar(config); @@ -158,6 +164,15 @@ export class ExtensionService implements RuleContext { return []; } + protected loadViewerActions(config: ExtensionConfig) { + if (config && config.features && config.features.viewer) { + return (config.features.viewer.actions || []).sort( + this.sortByOrder + ); + } + return []; + } + protected loadNavBar(config: ExtensionConfig): any { if (config && config.features) { return (config.features.navbar || []) @@ -296,7 +311,6 @@ export class ExtensionService implements RuleContext { return this.contentActions .filter(this.filterEnabled) .filter(action => this.filterByRules(action)) - .reduce(this.reduceSeparators, []) .map(action => { if (action.type === ContentActionType.menu) { const copy = this.copyAction(action); @@ -311,7 +325,14 @@ export class ExtensionService implements RuleContext { } return action; }) - .reduce(this.reduceEmptyMenus, []); + .reduce(this.reduceEmptyMenus, []) + .reduce(this.reduceSeparators, []); + } + + getViewerActions(): Array { + return this.viewerActions + .filter(this.filterEnabled) + .filter(action => this.filterByRules(action)); } reduceSeparators( @@ -320,6 +341,12 @@ export class ExtensionService implements RuleContext { i: number, arr: ContentActionRef[] ): ContentActionRef[] { + // remove leading separator + if (i === 0) { + if (arr[i].type === ContentActionType.separator) { + return acc; + } + } // remove duplicate separators if (i > 0) { const prev = arr[i - 1]; diff --git a/src/app/extensions/components/custom-component/demo.button.ts b/src/app/extensions/permission.extensions.ts similarity index 81% rename from src/app/extensions/components/custom-component/demo.button.ts rename to src/app/extensions/permission.extensions.ts index 84fb9ddb35..fb399cbe57 100644 --- a/src/app/extensions/components/custom-component/demo.button.ts +++ b/src/app/extensions/permission.extensions.ts @@ -23,14 +23,6 @@ * along with Alfresco. If not, see . */ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-demo-button', - template: ` - - ` -}) -export class DemoButtonComponent {} +export interface NodePermissions { + check(source: any, permissions: string[], options?: any): boolean; +} diff --git a/src/app/extensions/rule.extensions.ts b/src/app/extensions/rule.extensions.ts index 37ffd4d496..bbab0c3e29 100644 --- a/src/app/extensions/rule.extensions.ts +++ b/src/app/extensions/rule.extensions.ts @@ -25,6 +25,7 @@ import { SelectionState } from '../store/states'; import { NavigationState } from '../store/states/navigation.state'; +import { NodePermissions } from './permission.extensions'; export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean; @@ -32,6 +33,7 @@ export interface RuleContext { selection: SelectionState; navigation: NavigationState; evaluators: { [key: string]: RuleEvaluator }; + permissions: NodePermissions; } export class RuleRef { diff --git a/src/app/services/content-api.service.ts b/src/app/services/content-api.service.ts index 03708d8bfc..2adba098b5 100644 --- a/src/app/services/content-api.service.ts +++ b/src/app/services/content-api.service.ts @@ -39,7 +39,8 @@ import { SearchRequest, ResultSetPaging, SiteBody, - SiteEntry + SiteEntry, + FavoriteBody } from 'alfresco-js-api'; @Injectable() @@ -242,4 +243,30 @@ export class ContentApiService { this.api.sitesApi.getSite(siteId, opts) ); } + + addFavorite(nodes: Array): Observable { + const payload: FavoriteBody[] = nodes.map(node => { + const { isFolder, nodeId, id } = node.entry; + const siteId = node.entry['guid']; + const type = siteId ? 'site' : isFolder ? 'folder' : 'file'; + const guid = siteId || nodeId || id; + + return { + target: { + [type]: { + guid + } + } + }; + }); + + return Observable.from(this.api.favoritesApi.addFavorite('-me-', payload)); + } + + removeFavorite(nodes: Array): Observable { + return Observable.from(Promise.all(nodes.map(node => { + const id = node.entry.nodeId || node.entry.id; + return this.api.favoritesApi.removeFavoriteSite('-me-', id); + }))); + } } diff --git a/src/app/services/content-management.service.spec.ts b/src/app/services/content-management.service.spec.ts new file mode 100644 index 0000000000..a9687b2be7 --- /dev/null +++ b/src/app/services/content-management.service.spec.ts @@ -0,0 +1,1275 @@ +/*! + * @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 { TestBed, fakeAsync } from '@angular/core/testing'; +import { Observable } from 'rxjs/Rx'; +import { MatDialog, MatSnackBar } from '@angular/material'; +import { Actions, ofType, EffectsModule } from '@ngrx/effects'; +import { + SNACKBAR_INFO, SnackbarWarningAction, SnackbarInfoAction, + SnackbarErrorAction, SNACKBAR_ERROR, SNACKBAR_WARNING, PurgeDeletedNodesAction, + RestoreDeletedNodesAction, NavigateRouteAction, NAVIGATE_ROUTE, DeleteNodesAction, MoveNodesAction, CopyNodesAction +} from '../store/actions'; +import { map } from 'rxjs/operators'; +import { NodeEffects } from '../store/effects/node.effects'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { ContentApiService } from '../services/content-api.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../store/states'; +import { ContentManagementService } from './content-management.service'; +import { NodeActionsService } from './node-actions.service'; +import { TranslationService } from '@alfresco/adf-core'; + +describe('ContentManagementService', () => { + + let dialog: MatDialog; + let actions$: Actions; + let contentApi: ContentApiService; + let store: Store; + let contentManagementService: ContentManagementService; + let snackBar: MatSnackBar; + let nodeActions: NodeActionsService; + let translationService: TranslationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AppTestingModule, + EffectsModule.forRoot([NodeEffects]) + ] + }); + + contentApi = TestBed.get(ContentApiService); + actions$ = TestBed.get(Actions); + store = TestBed.get(Store); + contentManagementService = TestBed.get(ContentManagementService); + snackBar = TestBed.get(MatSnackBar); + nodeActions = TestBed.get(NodeActionsService); + translationService = TestBed.get(TranslationService); + + dialog = TestBed.get(MatDialog); + spyOn(dialog, 'open').and.returnValue({ + afterClosed() { + return Observable.of(true); + } + }); + }); + + describe('Copy node action', () => { + beforeEach(() => { + spyOn(snackBar, 'open').and.callThrough(); + }); + + it('notifies successful copy of a node', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + + it('notifies successful copy of multiple nodes', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy-1', name: 'name1' } }, + { entry: { id: 'node-to-copy-2', name: 'name2' } }]; + const createdItems = [ + { entry: { id: 'copy-of-node-1', name: 'name1' } }, + { entry: { id: 'copy-of-node-2', name: 'name2' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL'); + }); + + it('notifies partially copy of one node out of a multiple selection of nodes', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy-1', name: 'name1' } }, + { entry: { id: 'node-to-copy-2', name: 'name2' } }]; + const createdItems = [ + { entry: { id: 'copy-of-node-1', name: 'name1' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_SINGULAR'); + }); + + it('notifies partially copy of more nodes out of a multiple selection of nodes', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy-0', name: 'name0' } }, + { entry: { id: 'node-to-copy-1', name: 'name1' } }, + { entry: { id: 'node-to-copy-2', name: 'name2' } }]; + const createdItems = [ + { entry: { id: 'copy-of-node-0', name: 'name0' } }, + { entry: { id: 'copy-of-node-1', name: 'name1' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PARTIAL_PLURAL'); + }); + + it('notifies of failed copy of multiple nodes', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy-0', name: 'name0' } }, + { entry: { id: 'node-to-copy-1', name: 'name1' } }, + { entry: { id: 'node-to-copy-2', name: 'name2' } }]; + const createdItems = []; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_PLURAL'); + }); + + it('notifies of failed copy of one node', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + const selection = [ + { entry: { id: 'node-to-copy', name: 'name' } }]; + const createdItems = []; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.FAIL_SINGULAR'); + }); + + it('notifies error if success message was not emitted', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('')); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies permission error on copy of node', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new CopyNodesAction(selection)); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION'); + }); + + it('notifies generic error message on all errors, but 403', () => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + }); + + describe('Undo Copy action', () => { + beforeEach(() => { + spyOn(nodeActions, 'copyNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.COPY')); + + spyOn(snackBar, 'open').and.returnValue({ + onAction: () => Observable.of({}) + }); + }); + + it('should delete the newly created node on Undo action', () => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + + expect(contentApi.deleteNode).toHaveBeenCalledWith(createdItems[0].entry.id, { permanent: true }); + }); + + it('should delete also the node created inside an already existing folder from destination', () => { + const spyOnDeleteNode = spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); + + const selection = [ + { entry: { id: 'node-to-copy-1', name: 'name1' } }, + { entry: { id: 'node-to-copy-2', name: 'folder-with-name-already-existing-on-destination' } }]; + const id1 = 'copy-of-node-1'; + const id2 = 'copy-of-child-of-node-2'; + const createdItems = [ + { entry: { id: id1, name: 'name1' } }, + [ { entry: { id: id2, name: 'name-of-child-of-node-2' , parentId: 'the-folder-already-on-destination' } }] ]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_COPY.PLURAL'); + + expect(spyOnDeleteNode).toHaveBeenCalled(); + expect(spyOnDeleteNode.calls.allArgs()) + .toEqual([[id1, { permanent: true }], [id2, { permanent: true }]]); + }); + + it('notifies when error occurs on Undo action', () => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(contentApi.deleteNode).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + + it('notifies when some error of type Error occurs on Undo action', () => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error('oops!'))); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(contentApi.deleteNode).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + + it('notifies permission error when it occurs on Undo action', () => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + const selection = [{ entry: { id: 'node-to-copy-id', name: 'name' } }]; + const createdItems = [{ entry: { id: 'copy-id', name: 'name' } }]; + + store.dispatch(new CopyNodesAction(selection)); + nodeActions.contentCopied.next(createdItems); + + expect(nodeActions.copyNodes).toHaveBeenCalled(); + expect(contentApi.deleteNode).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toEqual('APP.MESSAGES.INFO.NODE_COPY.SINGULAR'); + }); + }); + + describe('Move node action', () => { + beforeEach(() => { + spyOn(translationService, 'instant').and.callFake((keysArray) => { + if (Array.isArray(keysArray)) { + const processedKeys = {}; + keysArray.forEach((key) => { + processedKeys[key] = key; + }); + return processedKeys; + } else { + return keysArray; + } + }); + }); + + beforeEach(() => { + spyOn(snackBar, 'open').and.callThrough(); + }); + + it('notifies successful move of a node', () => { + const node = [ { entry: { id: 'node-to-move-id', name: 'name' } } ]; + const moveResponse = { + succeeded: node, + failed: [], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = node; + store.dispatch(new MoveNodesAction(selection)); + + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); + }); + + it('notifies successful move of multiple nodes', () => { + const nodes = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }]; + const moveResponse = { + succeeded: nodes, + failed: [], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PLURAL'); + }); + + it('notifies partial move of a node', () => { + const nodes = [ { entry: { id: '1', name: 'name' } } ]; + const moveResponse = { + succeeded: [], + failed: [], + partiallySucceeded: nodes + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR'); + }); + + it('notifies partial move of multiple nodes', () => { + const nodes = [ + { entry: { id: '1', name: 'name' } }, + { entry: { id: '2', name: 'name2' } } ]; + const moveResponse = { + succeeded: [], + failed: [], + partiallySucceeded: nodes + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + const selection = nodes; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.PLURAL'); + }); + + it('notifies successful move and the number of nodes that could not be moved', () => { + const nodes = [ { entry: { id: '1', name: 'name' } }, + { entry: { id: '2', name: 'name2' } } ]; + const moveResponse = { + succeeded: [ nodes[0] ], + failed: [ nodes[1] ], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]) + .toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.FAIL'); + }); + + it('notifies successful move and the number of partially moved ones', () => { + const nodes = [ { entry: { id: '1', name: 'name' } }, + { entry: { id: '2', name: 'name2' } } ]; + const moveResponse = { + succeeded: [ nodes[0] ], + failed: [], + partiallySucceeded: [ nodes[1] ] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]) + .toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR APP.MESSAGES.INFO.NODE_MOVE.PARTIAL.SINGULAR'); + }); + + it('notifies error if success message was not emitted', () => { + const nodes = [{ entry: { id: 'node-to-move-id', name: 'name' } }]; + const moveResponse = { + succeeded: [], + failed: [], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('')); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies permission error on move of node', () => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.PERMISSION'); + }); + + it('notifies generic error message on all errors, but 403', () => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 404}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + + it('notifies conflict error message on 409', () => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 409}})))); + + const selection = [{ entry: { id: '1', name: 'name' } }]; + store.dispatch(new MoveNodesAction(selection)); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.NODE_MOVE'); + }); + + it('notifies error if move response has only failed items', () => { + const nodes = [ { entry: { id: '1', name: 'name' } } ]; + const moveResponse = { + succeeded: [], + failed: [ {} ], + partiallySucceeded: [] + }; + + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + spyOn(nodeActions, 'processResponse').and.returnValue(moveResponse); + + store.dispatch(new MoveNodesAction(nodes)); + nodeActions.contentMoved.next(moveResponse); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.ERRORS.GENERIC'); + }); + }); + + describe('Undo Move action', () => { + beforeEach(() => { + spyOn(translationService, 'instant').and.callFake((keysArray) => { + if (Array.isArray(keysArray)) { + const processedKeys = {}; + keysArray.forEach((key) => { + processedKeys[key] = key; + }); + return processedKeys; + } else { + return keysArray; + } + }); + }); + + beforeEach(() => { + spyOn(nodeActions, 'moveNodes').and.returnValue(Observable.of('OPERATION.SUCCES.CONTENT.MOVE')); + + spyOn(snackBar, 'open').and.returnValue({ + onAction: () => Observable.of({}) + }); + + // spyOn(snackBar, 'open').and.callThrough(); + }); + + it('should move node back to initial parent, after succeeded move', () => { + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; + const selection = [ node ]; + + spyOn(nodeActions, 'moveNodeAction').and.returnValue(Observable.of({})); + + store.dispatch(new MoveNodesAction(selection)); + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ { itemMoved: node, initialParentId: initialParent} ] + }; + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.moveNodeAction) + .toHaveBeenCalledWith(movedItems.succeeded[0].itemMoved.entry, movedItems.succeeded[0].initialParentId); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); + }); + + it('should move node back to initial parent, after succeeded move of a single file', () => { + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', isFolder: false, parentId: initialParent } }; + const selection = [ node ]; + + spyOn(nodeActions, 'moveNodeAction').and.returnValue(Observable.of({})); + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ node ] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.moveNodeAction).toHaveBeenCalledWith(node.entry, initialParent); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); + }); + + it('should restore deleted folder back to initial parent, after succeeded moving all its files', () => { + // when folder was deleted after all its children were moved to a folder with the same name from destination + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of(null)); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'folder-to-move-id', name: 'conflicting-name', parentId: initialParent, isFolder: true } }; + const selection = [ node ]; + + const itemMoved = {}; // folder was empty + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [ [ itemMoved ] ] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(contentApi.restoreNode).toHaveBeenCalled(); + expect(snackBar.open['calls'].argsFor(0)[0]).toBe('APP.MESSAGES.INFO.NODE_MOVE.SINGULAR'); + }); + + it('should notify when error occurs on Undo Move action', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'conflicting-name', parentId: initialParent } }; + const selection = [node]; + + const afterMoveParentId = 'parent-id-1'; + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name', parentId: afterMoveParentId } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(contentApi.restoreNode).toHaveBeenCalled(); + })); + + it('should notify when some error of type Error occurs on Undo Move action', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error('oops!'))); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; + const selection = [ node ]; + + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(contentApi.restoreNode).toHaveBeenCalled(); + })); + + it('should notify permission error when it occurs on Undo Move action', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(new Error(JSON.stringify({error: {statusCode: 403}})))); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const initialParent = 'parent-id-0'; + const node = { entry: { id: 'node-to-move-id', name: 'name', parentId: initialParent } }; + const selection = [ node ]; + + const childMoved = { entry: { id: 'child-of-node-to-move-id', name: 'child-name' } }; + nodeActions.moveDeletedEntries = [ node ]; // folder got deleted + + const movedItems = { + failed: [], + partiallySucceeded: [], + succeeded: [{ itemMoved: childMoved, initialParentId: initialParent }] + }; + + store.dispatch(new MoveNodesAction(selection)); + nodeActions.contentMoved.next(movedItems); + + expect(nodeActions.moveNodes).toHaveBeenCalled(); + expect(contentApi.restoreNode).toHaveBeenCalled(); + })); + }); + + describe('Delete action', () => { + it('should raise info message on successful single file deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => { + done(); + }) + ); + + const selection = [{ entry: { id: '1', name: 'name1' } }]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise error message on failed single file deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); + + const selection = [{ entry: { id: '1', name: 'name1' } }]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise info message on successful multiple files deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.of(null)); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise error message failed multiple files deletion', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.returnValue(Observable.throw(null)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise warning message when only one file is successful', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.callFake((id) => { + if (id === '1') { + return Observable.throw(null); + } else { + return Observable.of(null); + } + }); + + actions$.pipe( + ofType(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + + it('should raise warning message when some files are successfully deleted', fakeAsync(done => { + spyOn(contentApi, 'deleteNode').and.callFake((id) => { + if (id === '1') { + return Observable.throw(null); + } + + if (id === '2') { + return Observable.of(null); + } + + if (id === '3') { + return Observable.of(null); + } + }); + + actions$.pipe( + ofType(SNACKBAR_WARNING), + map(action => { + done(); + }) + ); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } } + ]; + + store.dispatch(new DeleteNodesAction(selection)); + })); + }); + + + describe('Permanent Delete', () => { + it('does not purge nodes if no selection', () => { + spyOn(contentApi, 'purgeDeletedNode'); + + store.dispatch(new PurgeDeletedNodesAction([])); + expect(contentApi.purgeDeletedNode).not.toHaveBeenCalled(); + }); + + it('call purge nodes if selection is not empty', fakeAsync(() => { + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); + + const selection = [ { entry: { id: '1' } } ]; + store.dispatch(new PurgeDeletedNodesAction(selection)); + + expect(contentApi.purgeDeletedNode).toHaveBeenCalled(); + })); + + describe('notification', () => { + it('raises warning on multiple fail and one success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_WARNING), + map((action: SnackbarWarningAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.throw({}); + } + + if (id === '3') { + return Observable.throw({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises warning on multiple success and multiple fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_WARNING), + map((action: SnackbarWarningAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.throw({}); + } + + if (id === '3') { + return Observable.throw({}); + } + + if (id === '4') { + return Observable.of({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } }, + { entry: { id: '4', name: 'name4' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises info on one selected node success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.of({})); + + const selection = [ + { entry: { id: '1', name: 'name1' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises error on one selected node fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + + spyOn(contentApi, 'purgeDeletedNode').and.returnValue(Observable.throw({})); + + const selection = [ + { entry: { id: '1', name: 'name1' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises info on all nodes success', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_INFO), + map((action: SnackbarInfoAction) => { + done(); + }) + ); + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.of({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + + it('raises error on all nodes fail', fakeAsync(done => { + actions$.pipe( + ofType(SNACKBAR_ERROR), + map((action: SnackbarErrorAction) => { + done(); + }) + ); + spyOn(contentApi, 'purgeDeletedNode').and.callFake((id) => { + if (id === '1') { + return Observable.throw({}); + } + + if (id === '2') { + return Observable.throw({}); + } + }); + + const selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + store.dispatch(new PurgeDeletedNodesAction(selection)); + })); + }); + }); + + describe('Restore Deleted', () => { + it('does not restore nodes if no selection', () => { + spyOn(contentApi, 'restoreNode'); + + const selection = []; + store.dispatch(new RestoreDeletedNodesAction(selection)); + + expect(contentApi.restoreNode).not.toHaveBeenCalled(); + }); + + it('does not restore nodes if selection has nodes without path', () => { + spyOn(contentApi, 'restoreNode'); + + const selection = [ { entry: { id: '1' } } ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + expect(contentApi.restoreNode).not.toHaveBeenCalled(); + }); + + it('call restore nodes if selection has nodes with path', fakeAsync(() => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ + list: { entries: [] } + })); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + expect(contentApi.restoreNode).toHaveBeenCalled(); + })); + + describe('refresh()', () => { + it('dispatch event on finish', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ + list: { entries: [] } + })); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + + contentManagementService.nodesRestored.subscribe(() => done()); + })); + }); + + describe('notification', () => { + beforeEach(() => { + spyOn(contentApi, 'getDeletedNodes').and.returnValue(Observable.of({ + list: { entries: [] } + })); + }); + + it('should raise error message on partial multiple fail ', fakeAsync(done => { + const error = { message: '{ "error": {} }' }; + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + spyOn(contentApi, 'restoreNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.throw(error); + } + + if (id === '3') { + return Observable.throw(error); + } + }); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } }, + { entry: { id: '2', name: 'name2', path } }, + { entry: { id: '3', name: 'name3', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('should raise error message when restored node exist, error 409', fakeAsync(done => { + const error = { message: '{ "error": { "statusCode": 409 } }' }; + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('should raise error message when restored node returns different statusCode', fakeAsync(done => { + const error = { message: '{ "error": { "statusCode": 404 } }' }; + + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('should raise error message when restored node location is missing', fakeAsync(done => { + const error = { message: '{ "error": { } }' }; + + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.throw(error)); + + actions$.pipe( + ofType(SNACKBAR_ERROR), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('should raise info message when restore multiple nodes', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.callFake((id) => { + if (id === '1') { + return Observable.of({}); + } + + if (id === '2') { + return Observable.of({}); + } + }); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } }, + { entry: { id: '2', name: 'name2', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + xit('should raise info message when restore selected node', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + + actions$.pipe( + ofType(SNACKBAR_INFO), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { entry: { id: '1', name: 'name1', path } } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + + it('navigate to restore selected node location onAction', fakeAsync(done => { + spyOn(contentApi, 'restoreNode').and.returnValue(Observable.of({})); + + actions$.pipe( + ofType(NAVIGATE_ROUTE), + map(action => done()) + ); + + const path = { + elements: [ + { + id: '1-1', + name: 'somewhere-over-the-rainbow' + } + ] + }; + + const selection = [ + { + entry: { + id: '1', + name: 'name1', + path + } + } + ]; + + store.dispatch(new RestoreDeletedNodesAction(selection)); + })); + }); + }); + +}); diff --git a/src/app/services/content-management.service.ts b/src/app/services/content-management.service.ts index fd5e38feda..74f903081c 100644 --- a/src/app/services/content-management.service.ts +++ b/src/app/services/content-management.service.ts @@ -25,11 +25,11 @@ import { Subject, Observable } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material'; -import { FolderDialogComponent, ConfirmDialogComponent } from '@alfresco/adf-content-services'; +import { MatDialog, MatSnackBar } from '@angular/material'; +import { FolderDialogComponent, ConfirmDialogComponent, ShareDialogComponent } from '@alfresco/adf-content-services'; import { LibraryDialogComponent } from '../dialogs/library/library.dialog'; import { SnackbarErrorAction, SnackbarInfoAction, SnackbarAction, SnackbarWarningAction, - NavigateRouteAction, SnackbarUserAction } from '../store/actions'; + NavigateRouteAction, SnackbarUserAction, UndoDeleteNodesAction, SetSelectedNodesAction } from '../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../store/states'; import { @@ -43,6 +43,11 @@ import { import { NodePermissionService } from './node-permission.service'; import { NodeInfo, DeletedNodeInfo, DeleteStatus } from '../store/models'; import { ContentApiService } from './content-api.service'; +import { sharedUrl } from '../store/selectors/app.selectors'; +import { NodeActionsService } from './node-actions.service'; +import { TranslationService } from '@alfresco/adf-core'; +import { NodePermissionsDialogComponent } from '../dialogs/node-permissions/node-permissions.dialog'; +import { NodeVersionsDialogComponent } from '../dialogs/node-versions/node-versions.dialog'; interface RestoredNode { status: number; @@ -61,14 +66,112 @@ export class ContentManagementService { libraryDeleted = new Subject(); libraryCreated = new Subject(); linksUnshared = new Subject(); + favoriteAdded = new Subject>(); + favoriteRemoved = new Subject>(); constructor( private store: Store, private contentApi: ContentApiService, private permission: NodePermissionService, - private dialogRef: MatDialog + private dialogRef: MatDialog, + private nodeActionsService: NodeActionsService, + private translation: TranslationService, + private snackBar: MatSnackBar ) {} + addFavorite(nodes: Array) { + if (nodes && nodes.length > 0) { + this.contentApi.addFavorite(nodes).subscribe(() => { + nodes.forEach(node => { + node.entry.isFavorite = true; + }); + this.store.dispatch(new SetSelectedNodesAction(nodes)); + this.favoriteAdded.next(nodes); + }); + } + } + + removeFavorite(nodes: Array) { + if (nodes && nodes.length > 0) { + this.contentApi.removeFavorite(nodes).subscribe(() => { + nodes.forEach(node => { + node.entry.isFavorite = false; + }); + this.store.dispatch(new SetSelectedNodesAction(nodes)); + this.favoriteRemoved.next(nodes); + }); + } + } + + managePermissions(node: MinimalNodeEntity): void { + if (node && node.entry) { + const { nodeId, id } = node.entry; + const siteId = node.entry['guid']; + const targetId = siteId || nodeId || id; + + if (targetId) { + this.dialogRef.open(NodePermissionsDialogComponent, { + data: { nodeId: targetId }, + panelClass: 'aca-permissions-dialog-panel', + width: '730px' + }); + } else { + this.store.dispatch( + new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION') + ); + } + } + } + + manageVersions(node: MinimalNodeEntity) { + if (node && node.entry) { + if (node.entry.nodeId) { + this.contentApi + .getNodeInfo(node.entry.nodeId) + .subscribe(entry => { + this.openVersionManagerDialog(entry); + }); + + } else { + this.openVersionManagerDialog(node.entry); + } + } + } + + private openVersionManagerDialog(node: MinimalNodeEntryEntity) { + // workaround Shared + if (node.isFile || node.nodeId) { + this.dialogRef.open(NodeVersionsDialogComponent, { + data: { node }, + panelClass: 'adf-version-manager-dialog-panel', + width: '630px' + }); + } else { + this.store.dispatch( + new SnackbarErrorAction('APP.MESSAGES.ERRORS.PERMISSION') + ); + } + } + + shareNode(node: MinimalNodeEntity): void { + if (node && node.entry && node.entry.isFile) { + + this.store + .select(sharedUrl) + .take(1) + .subscribe(baseShareUrl => { + this.dialogRef.open(ShareDialogComponent, { + width: '600px', + disableClose: true, + data: { + node, + baseShareUrl + } + }); + }); + } + } + createFolder(parentNodeId: string) { const dialogInstance = this.dialogRef.open(FolderDialogComponent, { data: { @@ -114,7 +217,7 @@ export class ContentManagementService { } createLibrary() { - const dialogInstance = this.dialogRef.open(LibraryDialogComponent, { + const dialogInstance = this.dialogRef.open(LibraryDialogComponent, { width: '400px' }); @@ -129,12 +232,30 @@ export class ContentManagementService { }); } - canDeleteNode(node: MinimalNodeEntity | Node): boolean { - return this.permission.check(node, ['delete']); + deleteLibrary(id: string): void { + this.contentApi.deleteSite(id).subscribe( + () => { + this.libraryDeleted.next(id); + this.store.dispatch( + new SnackbarInfoAction( + 'APP.MESSAGES.INFO.LIBRARY_DELETED' + ) + ); + }, + () => { + this.store.dispatch( + new SnackbarErrorAction( + 'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED' + ) + ); + } + ); } - canDeleteNodes(nodes: MinimalNodeEntity[]): boolean { - return this.permission.check(nodes, ['delete']); + async unshareNodes(links: Array) { + const promises = links.map(link => this.contentApi.deleteSharedLink(link.entry.id).toPromise()); + await Promise.all(promises); + this.linksUnshared.next(); } canUpdateNode(node: MinimalNodeEntity | Node): boolean { @@ -145,18 +266,6 @@ export class ContentManagementService { return this.permission.check(folderNode, ['create']); } - canDeleteSharedNodes(sharedLinks: MinimalNodeEntity[]): boolean { - return this.permission.check(sharedLinks, ['delete'], { - target: 'allowableOperationsOnTarget' - }); - } - - canUpdateSharedNode(sharedLink: MinimalNodeEntity): boolean { - return this.permission.check(sharedLink, ['update'], { - target: 'allowableOperationsOnTarget' - }); - } - purgeDeletedNodes(nodes: MinimalNodeEntity[]) { if (!nodes || nodes.length === 0) { return; @@ -226,6 +335,271 @@ export class ContentManagementService { }); } + copyNodes(nodes: Array) { + Observable.zip( + this.nodeActionsService.copyNodes(nodes), + this.nodeActionsService.contentCopied + ).subscribe( + result => { + const [operationResult, newItems] = result; + this.showCopyMessage(operationResult, nodes, newItems); + }, + error => { + this.showCopyMessage(error, nodes); + } + ); + } + + private showCopyMessage( + info: any, + nodes: Array, + newItems?: Array + ) { + const numberOfCopiedItems = newItems ? newItems.length : 0; + const failedItems = nodes.length - numberOfCopiedItems; + + let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; + + if (typeof info === 'string') { + if (info.toLowerCase().indexOf('succes') !== -1) { + let i18MessageSuffix; + + if (failedItems) { + if (numberOfCopiedItems) { + i18MessageSuffix = + numberOfCopiedItems === 1 + ? 'PARTIAL_SINGULAR' + : 'PARTIAL_PLURAL'; + } else { + i18MessageSuffix = + failedItems === 1 ? 'FAIL_SINGULAR' : 'FAIL_PLURAL'; + } + } else { + i18MessageSuffix = + numberOfCopiedItems === 1 ? 'SINGULAR' : 'PLURAL'; + } + + i18nMessageString = `APP.MESSAGES.INFO.NODE_COPY.${i18MessageSuffix}`; + } + } else { + try { + const { + error: { statusCode } + } = JSON.parse(info.message); + + if (statusCode === 403) { + i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; + } + } catch {} + } + + const undo = + numberOfCopiedItems > 0 + ? this.translation.instant('APP.ACTIONS.UNDO') + : ''; + + const message = this.translation.instant(i18nMessageString, { + success: numberOfCopiedItems, + failed: failedItems + }); + + this.snackBar + .open(message, undo, { + panelClass: 'info-snackbar', + duration: 3000 + }) + .onAction() + .subscribe(() => this.undoCopyNodes(newItems)); + } + + private undoCopyNodes(nodes: MinimalNodeEntity[]) { + const batch = this.nodeActionsService + .flatten(nodes) + .filter(item => item.entry) + .map(item => + this.contentApi.deleteNode(item.entry.id, { permanent: true }) + ); + + Observable.forkJoin(...batch).subscribe( + () => { + this.nodesDeleted.next(null); + }, + error => { + let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; + + let errorJson = null; + try { + errorJson = JSON.parse(error.message); + } catch {} + + if ( + errorJson && + errorJson.error && + errorJson.error.statusCode === 403 + ) { + i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; + } + + this.store.dispatch(new SnackbarErrorAction(i18nMessageString)); + } + ); + } + + moveNodes(nodes: Array) { + const permissionForMove = '!'; + + Observable.zip( + this.nodeActionsService.moveNodes(nodes, permissionForMove), + this.nodeActionsService.contentMoved + ).subscribe( + (result) => { + const [ operationResult, moveResponse ] = result; + this.showMoveMessage(nodes, operationResult, moveResponse); + + this.nodesMoved.next(null); + }, + (error) => { + this.showMoveMessage(nodes, error); + } + ); + } + + private undoMoveNodes(moveResponse, selectionParentId) { + const movedNodes = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'] : []; + const partiallyMovedNodes = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'] : []; + + const restoreDeletedNodesBatch = this.nodeActionsService.moveDeletedEntries + .map((folderEntry) => { + return this.contentApi + .restoreNode(folderEntry.nodeId || folderEntry.id) + .map(node => node.entry); + }); + + Observable.zip(...restoreDeletedNodesBatch, Observable.of(null)) + .flatMap(() => { + + const nodesToBeMovedBack = [...partiallyMovedNodes, ...movedNodes]; + + const revertMoveBatch = this.nodeActionsService + .flatten(nodesToBeMovedBack) + .filter(node => node.entry || (node.itemMoved && node.itemMoved.entry)) + .map((node) => { + if (node.itemMoved) { + return this.nodeActionsService.moveNodeAction(node.itemMoved.entry, node.initialParentId); + } else { + return this.nodeActionsService.moveNodeAction(node.entry, selectionParentId); + } + }); + + return Observable.zip(...revertMoveBatch, Observable.of(null)); + }) + .subscribe( + () => { + this.nodesMoved.next(null); + }, + error => { + let message = 'APP.MESSAGES.ERRORS.GENERIC'; + + let errorJson = null; + try { + errorJson = JSON.parse(error.message); + } catch {} + + if (errorJson && errorJson.error && errorJson.error.statusCode === 403) { + message = 'APP.MESSAGES.ERRORS.PERMISSION'; + } + + this.store.dispatch(new SnackbarErrorAction(message)); + } + ); + } + + deleteNodes(items: MinimalNodeEntity[]): void { + const batch: Observable[] = []; + + items.forEach(node => { + batch.push(this.deleteNode(node)); + }); + + Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => { + const status = this.processStatus(data); + const message = this.getDeleteMessage(status); + + if (message && status.someSucceeded) { + message.duration = 10000; + message.userAction = new SnackbarUserAction( + 'APP.ACTIONS.UNDO', + new UndoDeleteNodesAction([...status.success]) + ); + } + + this.store.dispatch(message); + + if (status.someSucceeded) { + this.nodesDeleted.next(); + } + }); + } + + undoDeleteNodes(items: DeletedNodeInfo[]): void { + const batch: Observable[] = []; + + items.forEach(item => { + batch.push(this.undoDeleteNode(item)); + }); + + Observable.forkJoin(...batch).subscribe(data => { + const processedData = this.processStatus(data); + + if (processedData.fail.length) { + const message = this.getUndoDeleteMessage(processedData); + this.store.dispatch(message); + } + + if (processedData.someSucceeded) { + this.nodesRestored.next(); + } + }); + } + + private undoDeleteNode(item: DeletedNodeInfo): Observable { + const { id, name } = item; + + return this.contentApi + .restoreNode(id) + .map(() => { + return { + id, + name, + status: 1 + }; + }) + .catch((error: any) => { + return Observable.of({ + id, + name, + status: 0 + }); + }); + } + + private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction { + if (status.someFailed && !status.oneFailed) { + return new SnackbarErrorAction( + 'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', + { number: status.fail.length } + ); + } + + if (status.oneFailed) { + return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', { + name: status.fail[0].name + }); + } + + return null; + } + private restoreNode(node: MinimalNodeEntity): Observable { const { entry } = node; @@ -457,4 +831,170 @@ export class ContentManagementService { } }); } + + private deleteNode(node: MinimalNodeEntity): Observable { + const { name } = node.entry; + const id = node.entry.nodeId || node.entry.id; + + + return this.contentApi + .deleteNode(id) + .map(() => { + return { + id, + name, + status: 1 + }; + }) + .catch(() => { + return Observable.of({ + id, + name, + status: 0 + }); + }); + } + + private getDeleteMessage(status: DeleteStatus): SnackbarAction { + if (status.allFailed && !status.oneFailed) { + return new SnackbarErrorAction( + 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', + { number: status.fail.length } + ); + } + + if (status.allSucceeded && !status.oneSucceeded) { + return new SnackbarInfoAction( + 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL', + { number: status.success.length } + ); + } + + if (status.someFailed && status.someSucceeded && !status.oneSucceeded) { + return new SnackbarWarningAction( + 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', + { + success: status.success.length, + failed: status.fail.length + } + ); + } + + if (status.someFailed && status.oneSucceeded) { + return new SnackbarWarningAction( + 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', + { + success: status.success.length, + failed: status.fail.length + } + ); + } + + if (status.oneFailed && !status.someSucceeded) { + return new SnackbarErrorAction( + 'APP.MESSAGES.ERRORS.NODE_DELETION', + { name: status.fail[0].name } + ); + } + + if (status.oneSucceeded && !status.someFailed) { + return new SnackbarInfoAction( + 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', + { name: status.success[0].name } + ); + } + + return null; + } + + private showMoveMessage(nodes: Array, info: any, moveResponse?: any) { + const succeeded = (moveResponse && moveResponse['succeeded']) ? moveResponse['succeeded'].length : 0; + const partiallySucceeded = (moveResponse && moveResponse['partiallySucceeded']) ? moveResponse['partiallySucceeded'].length : 0; + const failures = (moveResponse && moveResponse['failed']) ? moveResponse['failed'].length : 0; + + let successMessage = ''; + let partialSuccessMessage = ''; + let failedMessage = ''; + let errorMessage = ''; + + if (typeof info === 'string') { + + // in case of success + if (info.toLowerCase().indexOf('succes') !== -1) { + const i18nMessageString = 'APP.MESSAGES.INFO.NODE_MOVE.'; + let i18MessageSuffix = ''; + + if (succeeded) { + i18MessageSuffix = ( succeeded === 1 ) ? 'SINGULAR' : 'PLURAL'; + successMessage = `${i18nMessageString}${i18MessageSuffix}`; + } + + if (partiallySucceeded) { + i18MessageSuffix = ( partiallySucceeded === 1 ) ? 'PARTIAL.SINGULAR' : 'PARTIAL.PLURAL'; + partialSuccessMessage = `${i18nMessageString}${i18MessageSuffix}`; + } + + if (failures) { + // if moving failed for ALL nodes, emit error + if (failures === nodes.length) { + const errors = this.nodeActionsService.flatten(moveResponse['failed']); + errorMessage = this.getErrorMessage(errors[0]); + + } else { + i18MessageSuffix = 'PARTIAL.FAIL'; + failedMessage = `${i18nMessageString}${i18MessageSuffix}`; + } + } + } else { + errorMessage = 'APP.MESSAGES.ERRORS.GENERIC'; + } + + } else { + errorMessage = this.getErrorMessage(info); + } + + const undo = (succeeded + partiallySucceeded > 0) ? this.translation.instant('APP.ACTIONS.UNDO') : ''; + failedMessage = errorMessage ? errorMessage : failedMessage; + + const beforePartialSuccessMessage = (successMessage && partialSuccessMessage) ? ' ' : ''; + const beforeFailedMessage = ((successMessage || partialSuccessMessage) && failedMessage) ? ' ' : ''; + + const initialParentId = this.nodeActionsService.getEntryParentId(nodes[0].entry); + + const messages = this.translation.instant( + [successMessage, partialSuccessMessage, failedMessage], + { success: succeeded, failed: failures, partially: partiallySucceeded} + ); + + // TODO: review in terms of i18n + this.snackBar + .open( + messages[successMessage] + + beforePartialSuccessMessage + messages[partialSuccessMessage] + + beforeFailedMessage + messages[failedMessage] + , undo, { + panelClass: 'info-snackbar', + duration: 3000 + }) + .onAction() + .subscribe(() => this.undoMoveNodes(moveResponse, initialParentId)); + } + + getErrorMessage(errorObject): string { + let i18nMessageString = 'APP.MESSAGES.ERRORS.GENERIC'; + + try { + const { error: { statusCode } } = JSON.parse(errorObject.message); + + if (statusCode === 409) { + i18nMessageString = 'APP.MESSAGES.ERRORS.NODE_MOVE'; + + } else if (statusCode === 403) { + i18nMessageString = 'APP.MESSAGES.ERRORS.PERMISSION'; + } + + } catch (err) { /* Do nothing, keep the original message */ } + + return i18nMessageString; + } } diff --git a/src/app/services/node-permission.service.ts b/src/app/services/node-permission.service.ts index 3ef8ad765f..fa069834c1 100644 --- a/src/app/services/node-permission.service.ts +++ b/src/app/services/node-permission.service.ts @@ -24,9 +24,10 @@ */ import { Injectable } from '@angular/core'; +import { NodePermissions } from '../extensions/permission.extensions'; @Injectable() -export class NodePermissionService { +export class NodePermissionService implements NodePermissions { static DEFAULT_OPERATION = 'OR'; private defaultOptions = { @@ -34,8 +35,8 @@ export class NodePermissionService { target: null }; - check(source: any, permissions: string[], options: any = {}): boolean { - const opts = Object.assign({}, this.defaultOptions, options); + check(source: any, permissions: string[], options?: any): boolean { + const opts = Object.assign({}, this.defaultOptions, options || {}); if (source) { if (Array.isArray(source) && source.length) { diff --git a/src/app/store/actions.ts b/src/app/store/actions.ts index 013545cb3e..cacbc9c29b 100644 --- a/src/app/store/actions.ts +++ b/src/app/store/actions.ts @@ -24,6 +24,7 @@ */ export * from './actions/app.actions'; +export * from './actions/favorite.actions'; export * from './actions/node.actions'; export * from './actions/snackbar.actions'; export * from './actions/router.actions'; @@ -31,3 +32,4 @@ export * from './actions/viewer.actions'; export * from './actions/search.actions'; export * from './actions/user.actions'; export * from './actions/library.actions'; +export * from './actions/upload.actions'; diff --git a/src/app/store/actions/app.actions.ts b/src/app/store/actions/app.actions.ts index 33662ba145..81b3b9c2ab 100644 --- a/src/app/store/actions/app.actions.ts +++ b/src/app/store/actions/app.actions.ts @@ -33,6 +33,8 @@ export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER'; export const SET_SHARED_URL = 'SET_SHARED_URL'; export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER'; export const SET_CURRENT_URL = 'SET_CURRENT_URL'; +export const TOGGLE_INFO_DRAWER = 'TOGGLE_INFO_DRAWER'; +export const TOGGLE_DOCUMENT_DISPLAY_MODE = 'TOGGLE_DOCUMENT_DISPLAY_MODE'; export class SetAppNameAction implements Action { readonly type = SET_APP_NAME; @@ -68,3 +70,13 @@ export class SetCurrentUrlAction implements Action { readonly type = SET_CURRENT_URL; constructor(public payload: string) {} } + +export class ToggleInfoDrawerAction implements Action { + readonly type = TOGGLE_INFO_DRAWER; + constructor(public payload?: any) {} +} + +export class ToggleDocumentDisplayMode implements Action { + readonly type = TOGGLE_DOCUMENT_DISPLAY_MODE; + constructor(public payload?: any) {} +} diff --git a/src/app/directives/node-restore.directive.ts b/src/app/store/actions/favorite.actions.ts similarity index 67% rename from src/app/directives/node-restore.directive.ts rename to src/app/store/actions/favorite.actions.ts index 4380d6c342..c4f466166b 100644 --- a/src/app/directives/node-restore.directive.ts +++ b/src/app/store/actions/favorite.actions.ts @@ -23,23 +23,18 @@ * along with Alfresco. If not, see . */ -import { Directive, HostListener, Input } from '@angular/core'; +import { Action } from '@ngrx/store'; import { MinimalNodeEntity } from 'alfresco-js-api'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states'; -import { RestoreDeletedNodesAction } from '../store/actions'; -@Directive({ - selector: '[acaRestoreNode]' -}) -export class NodeRestoreDirective { - // tslint:disable-next-line:no-input-rename - @Input('acaRestoreNode') selection: MinimalNodeEntity[]; +export const ADD_FAVORITE = 'ADD_FAVORITE'; +export const REMOVE_FAVORITE = 'REMOVE_FAVORITE'; - constructor(private store: Store) {} +export class AddFavoriteAction implements Action { + readonly type = ADD_FAVORITE; + constructor(public payload: Array) {} +} - @HostListener('click') - onClick() { - this.store.dispatch(new RestoreDeletedNodesAction(this.selection)); - } +export class RemoveFavoriteAction implements Action { + readonly type = REMOVE_FAVORITE; + constructor(public payload: Array) {} } diff --git a/src/app/store/actions/library.actions.ts b/src/app/store/actions/library.actions.ts index bc65400285..1613286c87 100644 --- a/src/app/store/actions/library.actions.ts +++ b/src/app/store/actions/library.actions.ts @@ -30,7 +30,7 @@ export const CREATE_LIBRARY = 'CREATE_LIBRARY'; export class DeleteLibraryAction implements Action { readonly type = DELETE_LIBRARY; - constructor(public payload: string) {} + constructor(public payload?: string) {} } export class CreateLibraryAction implements Action { diff --git a/src/app/store/actions/node.actions.ts b/src/app/store/actions/node.actions.ts index f3e06ee117..5173c1158e 100644 --- a/src/app/store/actions/node.actions.ts +++ b/src/app/store/actions/node.actions.ts @@ -24,7 +24,6 @@ */ import { Action } from '@ngrx/store'; -import { NodeInfo } from '../models'; import { MinimalNodeEntity } from 'alfresco-js-api'; export const SET_SELECTED_NODES = 'SET_SELECTED_NODES'; @@ -35,6 +34,12 @@ export const PURGE_DELETED_NODES = 'PURGE_DELETED_NODES'; export const DOWNLOAD_NODES = 'DOWNLOAD_NODES'; export const CREATE_FOLDER = 'CREATE_FOLDER'; export const EDIT_FOLDER = 'EDIT_FOLDER'; +export const SHARE_NODE = 'SHARE_NODE'; +export const UNSHARE_NODES = 'UNSHARE_NODES'; +export const COPY_NODES = 'COPY_NODES'; +export const MOVE_NODES = 'MOVE_NODES'; +export const MANAGE_PERMISSIONS = 'MANAGE_PERMISSIONS'; +export const MANAGE_VERSIONS = 'MANAGE_VERSIONS'; export class SetSelectedNodesAction implements Action { readonly type = SET_SELECTED_NODES; @@ -43,7 +48,7 @@ export class SetSelectedNodesAction implements Action { export class DeleteNodesAction implements Action { readonly type = DELETE_NODES; - constructor(public payload: NodeInfo[] = []) {} + constructor(public payload: MinimalNodeEntity[] = []) {} } export class UndoDeleteNodesAction implements Action { @@ -75,3 +80,33 @@ export class EditFolderAction implements Action { readonly type = EDIT_FOLDER; constructor(public payload: MinimalNodeEntity) {} } + +export class ShareNodeAction implements Action { + readonly type = SHARE_NODE; + constructor(public payload: MinimalNodeEntity) {} +} + +export class UnshareNodesAction implements Action { + readonly type = UNSHARE_NODES; + constructor(public payload: Array) {} +} + +export class CopyNodesAction implements Action { + readonly type = COPY_NODES; + constructor(public payload: Array) {} +} + +export class MoveNodesAction implements Action { + readonly type = MOVE_NODES; + constructor(public payload: Array) {} +} + +export class ManagePermissionsAction implements Action { + readonly type = MANAGE_PERMISSIONS; + constructor(public payload: MinimalNodeEntity) {} +} + +export class ManageVersionsAction implements Action { + readonly type = MANAGE_VERSIONS; + constructor(public payload: MinimalNodeEntity) {} +} diff --git a/src/app/directives/node-permanent-delete.directive.ts b/src/app/store/actions/upload.actions.ts similarity index 62% rename from src/app/directives/node-permanent-delete.directive.ts rename to src/app/store/actions/upload.actions.ts index b32b5e895c..905cb5712a 100644 --- a/src/app/directives/node-permanent-delete.directive.ts +++ b/src/app/store/actions/upload.actions.ts @@ -23,27 +23,17 @@ * along with Alfresco. If not, see . */ -import { Directive, HostListener, Input } from '@angular/core'; -import { MinimalNodeEntity } from 'alfresco-js-api'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states'; -import { PurgeDeletedNodesAction } from '../store/actions'; +import { Action } from '@ngrx/store'; -@Directive({ - selector: '[acaPermanentDelete]' -}) -export class NodePermanentDeleteDirective { +export const UPLOAD_FILES = 'UPLOAD_FILES'; +export const UPLOAD_FOLDER = 'UPLOAD_FOLDER'; - // tslint:disable-next-line:no-input-rename - @Input('acaPermanentDelete') - selection: MinimalNodeEntity[]; - - constructor( - private store: Store - ) {} +export class UploadFilesAction implements Action { + readonly type = UPLOAD_FILES; + constructor(public payload: any) {} +} - @HostListener('click') - onClick() { - this.store.dispatch(new PurgeDeletedNodesAction(this.selection)); - } +export class UploadFolderAction implements Action { + readonly type = UPLOAD_FOLDER; + constructor(public payload: any) {} } diff --git a/src/app/store/app-store.module.ts b/src/app/store/app-store.module.ts index ecf31fd66f..7c66142db3 100644 --- a/src/app/store/app-store.module.ts +++ b/src/app/store/app-store.module.ts @@ -38,7 +38,9 @@ import { DownloadEffects, ViewerEffects, SearchEffects, - SiteEffects + SiteEffects, + UploadEffects, + FavoriteEffects } from './effects'; @NgModule({ @@ -55,7 +57,9 @@ import { DownloadEffects, ViewerEffects, SearchEffects, - SiteEffects + SiteEffects, + UploadEffects, + FavoriteEffects ]), !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 25 }) diff --git a/src/app/store/effects.ts b/src/app/store/effects.ts index 3c773fee26..b3e84d0752 100644 --- a/src/app/store/effects.ts +++ b/src/app/store/effects.ts @@ -24,9 +24,11 @@ */ export * from './effects/download.effects'; +export * from './effects/favorite.effects'; export * from './effects/node.effects'; export * from './effects/router.effects'; export * from './effects/snackbar.effects'; export * from './effects/viewer.effects'; export * from './effects/search.effects'; export * from './effects/library.effects'; +export * from './effects/upload.effects'; diff --git a/src/app/store/effects/favorite.effects.ts b/src/app/store/effects/favorite.effects.ts new file mode 100644 index 0000000000..43aff146c0 --- /dev/null +++ b/src/app/store/effects/favorite.effects.ts @@ -0,0 +1,82 @@ +/*! + * @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 { Effect, Actions, ofType } from '@ngrx/effects'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { ADD_FAVORITE, AddFavoriteAction, RemoveFavoriteAction, REMOVE_FAVORITE } from '../actions/favorite.actions'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../states'; +import { appSelection } from '../selectors/app.selectors'; +import { ContentManagementService } from '../../services/content-management.service'; + +@Injectable() +export class FavoriteEffects { + constructor( + private store: Store, + private actions$: Actions, + private content: ContentManagementService + ) {} + + @Effect({ dispatch: false }) + addFavorite$ = this.actions$.pipe( + ofType(ADD_FAVORITE), + map(action => { + if (action.payload && action.payload.length > 0) { + this.content.addFavorite(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.content.addFavorite(selection.nodes); + } + }); + } + }) + ); + + @Effect({ dispatch: false }) + removeFavorite$ = this.actions$.pipe( + ofType(REMOVE_FAVORITE), + map(action => { + if (action.payload && action.payload.length > 0) { + this.content.removeFavorite(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.content.removeFavorite(selection.nodes); + } + }); + } + }) + ); + + +} diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index d9debf63e5..1efe5c2604 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -30,21 +30,16 @@ import { DeleteLibraryAction, DELETE_LIBRARY, CreateLibraryAction, CREATE_LIBRARY } from '../actions'; -import { - SnackbarInfoAction, - SnackbarErrorAction -} from '../actions/snackbar.actions'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../states/app.state'; import { ContentManagementService } from '../../services/content-management.service'; -import { ContentApiService } from '../../services/content-api.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../states'; +import { appSelection } from '../selectors/app.selectors'; @Injectable() export class SiteEffects { constructor( - private actions$: Actions, private store: Store, - private contentApi: ContentApiService, + private actions$: Actions, private content: ContentManagementService ) {} @@ -53,7 +48,16 @@ export class SiteEffects { ofType(DELETE_LIBRARY), map(action => { if (action.payload) { - this.deleteLibrary(action.payload); + this.content.deleteLibrary(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && selection.library) { + this.content.deleteLibrary(selection.library.entry.id); + } + }); } }) ); @@ -62,31 +66,7 @@ export class SiteEffects { createLibrary$ = this.actions$.pipe( ofType(CREATE_LIBRARY), map(action => { - this.createLibrary(); + this.content.createLibrary(); }) ); - - private deleteLibrary(id: string) { - this.contentApi.deleteSite(id).subscribe( - () => { - this.content.libraryDeleted.next(id); - this.store.dispatch( - new SnackbarInfoAction( - 'APP.MESSAGES.INFO.LIBRARY_DELETED' - ) - ); - }, - () => { - this.store.dispatch( - new SnackbarErrorAction( - 'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED' - ) - ); - } - ); - } - - private createLibrary() { - this.content.createLibrary(); - } } diff --git a/src/app/store/effects/node.effects.ts b/src/app/store/effects/node.effects.ts index 95794d3c2e..5a43ffd00b 100644 --- a/src/app/store/effects/node.effects.ts +++ b/src/app/store/effects/node.effects.ts @@ -29,49 +29,97 @@ import { map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppStore } from '../states/app.state'; import { - SnackbarWarningAction, - SnackbarInfoAction, - SnackbarErrorAction, PurgeDeletedNodesAction, PURGE_DELETED_NODES, DeleteNodesAction, DELETE_NODES, - SnackbarUserAction, - SnackbarAction, UndoDeleteNodesAction, UNDO_DELETE_NODES, CreateFolderAction, - CREATE_FOLDER + CREATE_FOLDER, + EditFolderAction, + EDIT_FOLDER, + RestoreDeletedNodesAction, + RESTORE_DELETED_NODES, + ShareNodeAction, + SHARE_NODE } from '../actions'; import { ContentManagementService } from '../../services/content-management.service'; -import { Observable } from 'rxjs/Rx'; -import { NodeInfo, DeleteStatus, DeletedNodeInfo } from '../models'; -import { ContentApiService } from '../../services/content-api.service'; import { currentFolder, appSelection } from '../selectors/app.selectors'; -import { EditFolderAction, EDIT_FOLDER, RestoreDeletedNodesAction, RESTORE_DELETED_NODES } from '../actions/node.actions'; +import { + UnshareNodesAction, + UNSHARE_NODES, + CopyNodesAction, + COPY_NODES, + MoveNodesAction, + MOVE_NODES, + ManagePermissionsAction, + MANAGE_PERMISSIONS, + ManageVersionsAction, + MANAGE_VERSIONS +} from '../actions/node.actions'; @Injectable() export class NodeEffects { constructor( private store: Store, private actions$: Actions, - private contentManagementService: ContentManagementService, - private contentApi: ContentApiService + private contentService: ContentManagementService ) {} + @Effect({ dispatch: false }) + shareNode$ = this.actions$.pipe( + ofType(SHARE_NODE), + map(action => { + if (action.payload) { + this.contentService.shareNode(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && selection.file) { + this.contentService.shareNode(selection.file); + } + }); + } + }) + ); + + @Effect({ dispatch: false }) + unshareNodes$ = this.actions$.pipe( + ofType(UNSHARE_NODES), + map(action => { + if (action && action.payload && action.payload.length > 0) { + this.contentService.unshareNodes(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.contentService.unshareNodes(selection.nodes); + } + }); + } + }) + ); + @Effect({ dispatch: false }) purgeDeletedNodes$ = this.actions$.pipe( ofType(PURGE_DELETED_NODES), map(action => { if (action && action.payload && action.payload.length > 0) { - this.contentManagementService.purgeDeletedNodes(action.payload); + this.contentService.purgeDeletedNodes(action.payload); } else { this.store .select(appSelection) .take(1) .subscribe(selection => { if (selection && selection.count > 0) { - this.contentManagementService.purgeDeletedNodes(selection.nodes); + this.contentService.purgeDeletedNodes( + selection.nodes + ); } }); } @@ -83,14 +131,16 @@ export class NodeEffects { ofType(RESTORE_DELETED_NODES), map(action => { if (action && action.payload && action.payload.length > 0) { - this.contentManagementService.restoreDeletedNodes(action.payload); + this.contentService.restoreDeletedNodes(action.payload); } else { this.store .select(appSelection) .take(1) .subscribe(selection => { if (selection && selection.count > 0) { - this.contentManagementService.restoreDeletedNodes(selection.nodes); + this.contentService.restoreDeletedNodes( + selection.nodes + ); } }); } @@ -101,8 +151,17 @@ export class NodeEffects { deleteNodes$ = this.actions$.pipe( ofType(DELETE_NODES), map(action => { - if (action.payload.length > 0) { - this.deleteNodes(action.payload); + if (action && action.payload && action.payload.length > 0) { + this.contentService.deleteNodes(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && selection.count > 0) { + this.contentService.deleteNodes(selection.nodes); + } + }); } }) ); @@ -112,7 +171,7 @@ export class NodeEffects { ofType(UNDO_DELETE_NODES), map(action => { if (action.payload.length > 0) { - this.undoDeleteNodes(action.payload); + this.contentService.undoDeleteNodes(action.payload); } }) ); @@ -122,14 +181,14 @@ export class NodeEffects { ofType(CREATE_FOLDER), map(action => { if (action.payload) { - this.contentManagementService.createFolder(action.payload); + this.contentService.createFolder(action.payload); } else { this.store .select(currentFolder) .take(1) .subscribe(node => { if (node && node.id) { - this.contentManagementService.createFolder(node.id); + this.contentService.createFolder(node.id); } }); } @@ -141,215 +200,97 @@ export class NodeEffects { ofType(EDIT_FOLDER), map(action => { if (action.payload) { - this.contentManagementService.editFolder(action.payload); + this.contentService.editFolder(action.payload); } else { this.store .select(appSelection) .take(1) .subscribe(selection => { if (selection && selection.folder) { - this.contentManagementService.editFolder(selection.folder); + this.contentService.editFolder(selection.folder); } }); } }) ); - private deleteNodes(items: NodeInfo[]): void { - const batch: Observable[] = []; - - items.forEach(node => { - batch.push(this.deleteNode(node)); - }); - - Observable.forkJoin(...batch).subscribe((data: DeletedNodeInfo[]) => { - const status = this.processStatus(data); - const message = this.getDeleteMessage(status); - - if (message && status.someSucceeded) { - message.duration = 10000; - message.userAction = new SnackbarUserAction( - 'APP.ACTIONS.UNDO', - new UndoDeleteNodesAction([...status.success]) - ); - } - - this.store.dispatch(message); - - if (status.someSucceeded) { - this.contentManagementService.nodesDeleted.next(); - } - }); - } - - private deleteNode(node: NodeInfo): Observable { - const { id, name } = node; - - return this.contentApi - .deleteNode(id) - .map(() => { - return { - id, - name, - status: 1 - }; - }) - .catch((error: any) => { - return Observable.of({ - id, - name, - status: 0 - }); - }); - } - - private getDeleteMessage(status: DeleteStatus): SnackbarAction { - if (status.allFailed && !status.oneFailed) { - return new SnackbarErrorAction( - 'APP.MESSAGES.ERRORS.NODE_DELETION_PLURAL', - { number: status.fail.length } - ); - } - - if (status.allSucceeded && !status.oneSucceeded) { - return new SnackbarInfoAction( - 'APP.MESSAGES.INFO.NODE_DELETION.PLURAL', - { number: status.success.length } - ); - } - - if (status.someFailed && status.someSucceeded && !status.oneSucceeded) { - return new SnackbarWarningAction( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_PLURAL', - { - success: status.success.length, - failed: status.fail.length - } - ); - } - - if (status.someFailed && status.oneSucceeded) { - return new SnackbarWarningAction( - 'APP.MESSAGES.INFO.NODE_DELETION.PARTIAL_SINGULAR', - { - success: status.success.length, - failed: status.fail.length - } - ); - } - - if (status.oneFailed && !status.someSucceeded) { - return new SnackbarErrorAction( - 'APP.MESSAGES.ERRORS.NODE_DELETION', - { name: status.fail[0].name } - ); - } - - if (status.oneSucceeded && !status.someFailed) { - return new SnackbarInfoAction( - 'APP.MESSAGES.INFO.NODE_DELETION.SINGULAR', - { name: status.success[0].name } - ); - } - - return null; - } - - private undoDeleteNodes(items: DeletedNodeInfo[]): void { - const batch: Observable[] = []; - - items.forEach(item => { - batch.push(this.undoDeleteNode(item)); - }); - - Observable.forkJoin(...batch).subscribe(data => { - const processedData = this.processStatus(data); - - if (processedData.fail.length) { - const message = this.getUndoDeleteMessage(processedData); - this.store.dispatch(message); + @Effect({ dispatch: false }) + copyNodes$ = this.actions$.pipe( + ofType(COPY_NODES), + map(action => { + if (action.payload && action.payload.length > 0) { + this.contentService.copyNodes(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.contentService.copyNodes(selection.nodes); + } + }); } + }) + ); - if (processedData.someSucceeded) { - this.contentManagementService.nodesRestored.next(); + @Effect({ dispatch: false }) + moveNodes$ = this.actions$.pipe( + ofType(MOVE_NODES), + map(action => { + if (action.payload && action.payload.length > 0) { + this.contentService.moveNodes(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.contentService.moveNodes(selection.nodes); + } + }); } - }); - } - - private undoDeleteNode(item: DeletedNodeInfo): Observable { - const { id, name } = item; - - return this.contentApi - .restoreNode(id) - .map(() => { - return { - id, - name, - status: 1 - }; - }) - .catch((error: any) => { - return Observable.of({ - id, - name, - status: 0 - }); - }); - } - - private getUndoDeleteMessage(status: DeleteStatus): SnackbarAction { - if (status.someFailed && !status.oneFailed) { - return new SnackbarErrorAction( - 'APP.MESSAGES.ERRORS.NODE_RESTORE_PLURAL', - { number: status.fail.length } - ); - } - - if (status.oneFailed) { - return new SnackbarErrorAction('APP.MESSAGES.ERRORS.NODE_RESTORE', { - name: status.fail[0].name - }); - } - - return null; - } + }) + ); - private processStatus(data: DeletedNodeInfo[] = []): DeleteStatus { - const status = { - fail: [], - success: [], - get someFailed() { - return !!this.fail.length; - }, - get someSucceeded() { - return !!this.success.length; - }, - get oneFailed() { - return this.fail.length === 1; - }, - get oneSucceeded() { - return this.success.length === 1; - }, - get allSucceeded() { - return this.someSucceeded && !this.someFailed; - }, - get allFailed() { - return this.someFailed && !this.someSucceeded; - }, - reset() { - this.fail = []; - this.success = []; + @Effect({ dispatch: false }) + managePermissions = this.actions$.pipe( + ofType(MANAGE_PERMISSIONS), + map(action => { + if (action && action.payload) { + this.contentService.managePermissions(action.payload); + } else { + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && !selection.isEmpty) { + this.contentService.managePermissions( + selection.first + ); + } + }); } - }; + }) + ); - return data.reduce((acc, node) => { - if (node.status) { - acc.success.push(node); + @Effect({ dispatch: false }) + manageVersions$ = this.actions$.pipe( + ofType(MANAGE_VERSIONS), + map(action => { + if (action && action.payload) { + this.contentService.manageVersions(action.payload); } else { - acc.fail.push(node); + this.store + .select(appSelection) + .take(1) + .subscribe(selection => { + if (selection && selection.file) { + this.contentService.manageVersions( + selection.file + ); + } + }); } - - return acc; - }, status); - } + }) + ); } diff --git a/src/app/store/effects/upload.effects.ts b/src/app/store/effects/upload.effects.ts new file mode 100644 index 0000000000..0f525b5a59 --- /dev/null +++ b/src/app/store/effects/upload.effects.ts @@ -0,0 +1,116 @@ +/*! + * @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 { Injectable, RendererFactory2, NgZone } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../states'; +import { UploadFilesAction, UPLOAD_FILES } from '../actions'; +import { map } from 'rxjs/operators'; +import { FileUtils, FileModel, UploadService } from '@alfresco/adf-core'; +import { currentFolder } from '../selectors/app.selectors'; +import { UploadFolderAction, UPLOAD_FOLDER } from '../actions/upload.actions'; + +@Injectable() +export class UploadEffects { + private fileInput: HTMLInputElement; + private folderInput: HTMLInputElement; + + constructor( + private store: Store, + private actions$: Actions, + private ngZone: NgZone, + private uploadService: UploadService, + rendererFactory: RendererFactory2 + ) { + const renderer = rendererFactory.createRenderer(null, null); + + this.fileInput = renderer.createElement('input') as HTMLInputElement; + this.fileInput.id = 'app-upload-files'; + this.fileInput.type = 'file'; + this.fileInput.style.display = 'none'; + this.fileInput.setAttribute('multiple', ''); + this.fileInput.addEventListener('change', event => this.upload(event)); + renderer.appendChild(document.body, this.fileInput); + + + this.folderInput = renderer.createElement('input') as HTMLInputElement; + this.folderInput.id = 'app-upload-folder'; + this.folderInput.type = 'file'; + this.folderInput.style.display = 'none'; + this.folderInput.setAttribute('directory', ''); + this.folderInput.setAttribute('webkitdirectory', ''); + this.folderInput.addEventListener('change', event => this.upload(event)); + renderer.appendChild(document.body, this.folderInput); + } + + @Effect({ dispatch: false }) + uploadFiles$ = this.actions$.pipe( + ofType(UPLOAD_FILES), + map(() => { + this.fileInput.click(); + }) + ); + + @Effect({ dispatch: false }) + uploadFolder$ = this.actions$.pipe( + ofType(UPLOAD_FOLDER), + map(() => { + this.folderInput.click(); + }) + ); + + private upload(event: any): void { + this.store + .select(currentFolder) + .take(1) + .subscribe(node => { + if (node && node.id) { + const input = event.currentTarget; + const files = FileUtils.toFileArray(input.files).map( + file => { + return new FileModel(file, { + parentId: node.id, + path: (file.webkitRelativePath || '').replace(/\/[^\/]*$/, ''), + nodeType: 'cm:content' + }); + } + ); + + this.uploadQueue(files); + event.target.value = ''; + } + }); + } + + private uploadQueue(files: FileModel[]) { + if (files.length > 0) { + this.ngZone.run(() => { + this.uploadService.addToQueue(...files); + this.uploadService.uploadFilesInTheQueue(); + }); + } + } +} diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts index 2d789c5a9b..864b53eb46 100644 --- a/src/app/store/reducers/app.reducer.ts +++ b/src/app/store/reducers/app.reducer.ts @@ -42,8 +42,15 @@ import { SetSharedUrlAction, SET_CURRENT_FOLDER, SetCurrentFolderAction, - SET_CURRENT_URL, SetCurrentUrlAction + SET_CURRENT_URL, + SetCurrentUrlAction } from '../actions'; +import { + TOGGLE_INFO_DRAWER, + ToggleInfoDrawerAction, + TOGGLE_DOCUMENT_DISPLAY_MODE, + ToggleDocumentDisplayMode +} from '../actions/app.actions'; export function appReducer( state: AppState = INITIAL_APP_STATE, @@ -85,6 +92,14 @@ export function appReducer( case SET_CURRENT_URL: newState = updateCurrentUrl(state, action); break; + case TOGGLE_INFO_DRAWER: + newState = updateInfoDrawer(state, action); + break; + case TOGGLE_DOCUMENT_DISPLAY_MODE: + newState = updateDocumentDisplayMode(state, < + ToggleDocumentDisplayMode + >action); + break; default: newState = Object.assign({}, state); } @@ -168,6 +183,31 @@ function updateCurrentUrl(state: AppState, action: SetCurrentUrlAction) { return newState; } +function updateInfoDrawer(state: AppState, action: ToggleInfoDrawerAction) { + const newState = Object.assign({}, state); + + let value = state.infoDrawerOpened; + if (state.selection.isEmpty) { + value = false; + } else { + value = !value; + } + + newState.infoDrawerOpened = value; + + return newState; +} + +function updateDocumentDisplayMode( + state: AppState, + action: ToggleDocumentDisplayMode +) { + const newState = Object.assign({}, state); + newState.documentDisplayMode = + newState.documentDisplayMode === 'list' ? 'gallery' : 'list'; + return newState; +} + function updateSelectedNodes( state: AppState, action: SetSelectedNodesAction @@ -181,6 +221,7 @@ function updateSelectedNodes( let last = null; let file = null; let folder = null; + let library = null; if (nodes.length > 0) { first = nodes[0]; @@ -197,6 +238,15 @@ function updateSelectedNodes( } } + const libraries = [...action.payload].filter((node: any) => node.isLibrary); + if (libraries.length === 1) { + library = libraries[0]; + } + + if (isEmpty) { + newState.infoDrawerOpened = false; + } + newState.selection = { count, nodes, @@ -204,7 +254,9 @@ function updateSelectedNodes( first, last, file, - folder + folder, + libraries, + library }; return newState; } diff --git a/src/app/store/selectors/app.selectors.ts b/src/app/store/selectors/app.selectors.ts index 4e5f8788c6..dc2af14fdf 100644 --- a/src/app/store/selectors/app.selectors.ts +++ b/src/app/store/selectors/app.selectors.ts @@ -36,6 +36,8 @@ export const selectUser = createSelector(selectApp, state => state.user); export const sharedUrl = createSelector(selectApp, state => state.sharedUrl); export const appNavigation = createSelector(selectApp, state => state.navigation); export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder); +export const infoDrawerOpened = createSelector(selectApp, state => state.infoDrawerOpened); +export const documentDisplayMode = createSelector(selectApp, state => state.documentDisplayMode); export const selectionWithFolder = createSelector( appSelection, diff --git a/src/app/store/states/app.state.ts b/src/app/store/states/app.state.ts index 2b134d029f..4002a92911 100644 --- a/src/app/store/states/app.state.ts +++ b/src/app/store/states/app.state.ts @@ -36,6 +36,8 @@ export interface AppState { selection: SelectionState; user: ProfileState; navigation: NavigationState; + infoDrawerOpened: boolean; + documentDisplayMode: string; } export const INITIAL_APP_STATE: AppState = { @@ -52,12 +54,15 @@ export const INITIAL_APP_STATE: AppState = { }, selection: { nodes: [], + libraries: [], isEmpty: true, count: 0 }, navigation: { currentFolder: null - } + }, + infoDrawerOpened: false, + documentDisplayMode: 'list' }; export interface AppStore { diff --git a/src/app/store/states/selection.state.ts b/src/app/store/states/selection.state.ts index 9db2061926..b822baae14 100644 --- a/src/app/store/states/selection.state.ts +++ b/src/app/store/states/selection.state.ts @@ -23,14 +23,16 @@ * along with Alfresco. If not, see . */ -import { MinimalNodeEntity } from 'alfresco-js-api'; +import { MinimalNodeEntity, SiteEntry } from 'alfresco-js-api'; export interface SelectionState { count: number; nodes: MinimalNodeEntity[]; + libraries: SiteEntry[]; isEmpty: boolean; first?: MinimalNodeEntity; last?: MinimalNodeEntity; folder?: MinimalNodeEntity; file?: MinimalNodeEntity; + library?: SiteEntry; } diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 44a1a63b50..7e408273ab 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -8,6 +8,85 @@ ], "rules": [ + { + "id": "app.toolbar.favorite.canToggle", + "comment": "workaround for recent files and search api issue", + "type": "core.every", + "parameters": [ + { + "type": "rule", + "value": "core.some", + "parameters": [ + { "type": "rule", "value": "app.selection.canAddFavorite" }, + { "type": "rule", "value": "app.selection.canRemoveFavorite" } + ] + }, + { + "type": "rule", + "value": "core.some", + "parameters": [ + { "type": "rule", "value": "app.navigation.isRecentFiles" }, + { "type": "rule", "value": "app.navigation.isSharedFiles" }, + { "type": "rule", "value": "app.navigation.isSearchResults" } + ] + } + ] + }, + { + "id": "app.toolbar.favorite.canAdd", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.canAddFavorite" }, + { "type": "rule", "value": "app.navigation.isNotRecentFiles" }, + { "type": "rule", "value": "app.navigation.isNotSharedFiles" }, + { "type": "rule", "value": "app.navigation.isNotSearchResults" } + ] + }, + { + "id": "app.toolbar.favorite.canRemove", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.canRemoveFavorite" }, + { "type": "rule", "value": "app.navigation.isNotRecentFiles" }, + { "type": "rule", "value": "app.navigation.isNotSharedFiles" }, + { "type": "rule", "value": "app.navigation.isNotSearchResults" } + ] + }, + { + "id": "app.toolbar.info", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.notEmpty" }, + { "type": "rule", "value": "app.navigation.isNotLibraries" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" } + ] + }, + { + "id": "app.toolbar.canCopyNode", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.notEmpty" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" }, + { "type": "rule", "value": "app.navigation.isNotLibraries" } + ] + }, + { + "id": "app.toolbar.permissions", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.file" }, + { "type": "rule", "value": "app.selection.first.canUpdate" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" } + ] + }, + { + "id": "app.toolbar.versions", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.file" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" } + ] + }, { "id": "app.trashcan.hasSelection", "type": "core.every", @@ -21,7 +100,8 @@ "type": "core.every", "parameters": [ { "type": "rule", "value": "app.selection.folder" }, - { "type": "rule", "value": "app.selection.folder.canUpdate" } + { "type": "rule", "value": "app.selection.folder.canUpdate" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" } ] }, { @@ -57,13 +137,43 @@ "id": "app.create.folder", "type": "default", "icon": "create_new_folder", - "title": "ext: Create Folder", + "title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER", + "description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", + "description-disabled": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED", "actions": { "click": "CREATE_FOLDER" }, "rules": { "enabled": "app.navigation.folder.canCreate" } + }, + { + "id": "app.create.uploadFile", + "type": "default", + "icon": "file_upload", + "title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE", + "description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES", + "description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED", + "actions": { + "click": "UPLOAD_FILES" + }, + "rules": { + "enabled": "app.navigation.folder.canUpload" + } + }, + { + "id": "app.create.uploadFolder", + "type": "default", + "icon": "file_upload", + "title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER", + "description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS", + "description-disabled": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED", + "actions": { + "click": "UPLOAD_FOLDER" + }, + "rules": { + "enabled": "app.navigation.folder.canUpload" + } } ], "navbar": [ @@ -122,24 +232,6 @@ ], "content": { "actions": [ - { - "id": "app.toolbar.separator.1", - "order": 5, - "type": "separator" - }, - { - "id": "app.toolbar.createFolder", - "type": "button", - "order": 10, - "title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", - "icon": "create_new_folder", - "actions": { - "click": "CREATE_FOLDER" - }, - "rules": { - "visible": "app.navigation.folder.canCreate" - } - }, { "id": "app.toolbar.preview", "type": "button", @@ -204,16 +296,261 @@ } }, { - "id": "app.toolbar.separator.2", - "order": 200, - "type": "separator" + "id": "app.toolbar.createLibrary", + "type": "button", + "title": "Create Library", + "icon": "create_new_folder", + "actions": { + "click": "CREATE_LIBRARY" + }, + "rules": { + "visible": "app.navigation.isLibraries" + } }, { - "disabled": true, - "id": "app.toolbar.custom.1", - "order": 200, + "id": "app.toolbar.info", "type": "custom", - "component": "app.demo.button" + "component": "app.toolbar.toggleInfoDrawer", + "rules": { + "visible": "app.toolbar.info" + } + }, + { + "id": "app.toolbar.more", + "type": "menu", + "icon": "more_vert", + "title": "APP.ACTIONS.MORE", + "children": [ + { + "id": "app.toolbar.favorite", + "comment": "workaround for Recent Files and Search API issue", + "type": "custom", + "component": "app.toolbar.toggleFavorite", + "rules": { + "visible": "app.toolbar.favorite.canToggle" + } + }, + { + "id": "app.toolbar.favorite.add", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "icon": "star_border", + "actions": { + "click": "ADD_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canAdd" + } + }, + { + "id": "app.toolbar.favorite.remove", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "icon": "star", + "actions": { + "click": "REMOVE_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canRemove" + } + }, + { + "id": "app.toolbar.copy", + "type": "button", + "title": "APP.ACTIONS.COPY", + "icon": "content_copy", + "actions": { + "click": "COPY_NODES" + }, + "rules": { + "visible": "app.toolbar.canCopyNode" + } + }, + { + "id": "app.toolbar.move", + "type": "button", + "title": "APP.ACTIONS.MOVE", + "icon": "library_books", + "actions": { + "click": "MOVE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.toolbar.share", + "type": "button", + "title": "APP.ACTIONS.SHARE", + "icon": "share", + "actions": { + "click": "SHARE_NODE" + }, + "rules": { + "visible": "app.selection.file.canShare" + } + }, + { + "id": "app.toolbar.unshare", + "type": "button", + "title": "APP.ACTIONS.UNSHARE", + "icon": "stop_screen_share", + "actions": { + "click": "UNSHARE_NODES" + }, + "rules": { + "visible": "app.selection.canUnshare" + } + }, + { + "id": "app.toolbar.delete", + "type": "button", + "title": "APP.ACTIONS.DELETE", + "icon": "delete", + "actions": { + "click": "DELETE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.toolbar.deleteLibrary", + "type": "button", + "title": "APP.ACTIONS.DELETE", + "icon": "delete", + "actions": { + "click": "DELETE_LIBRARY" + }, + "rules": { + "visible": "app.selection.library" + } + }, + { + "id": "app.toolbar.versions", + "type": "button", + "title": "APP.ACTIONS.VERSIONS", + "icon": "history", + "actions": { + "click": "MANAGE_VERSIONS" + }, + "rules": { + "visible": "app.toolbar.versions" + } + }, + { + "id": "app.toolbar.permissions", + "type": "button", + "title": "APP.ACTIONS.PERMISSIONS", + "icon": "settings_input_component", + "actions": { + "click": "MANAGE_PERMISSIONS" + }, + "rules": { + "visible": "app.toolbar.permissions" + } + } + ] + } + ] + }, + "viewer": { + "actions": [ + { + "id": "app.viewer.favorite.add", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "icon": "star_border", + "actions": { + "click": "ADD_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canAdd" + } + }, + { + "id": "app.viewer.favorite.remove", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "icon": "star", + "actions": { + "click": "REMOVE_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canRemove" + } + }, + { + "id": "app.viewer.share", + "type": "button", + "title": "APP.ACTIONS.SHARE", + "icon": "share", + "actions": { + "click": "SHARE_NODE" + }, + "rules": { + "visible": "app.selection.file.canShare" + } + }, + { + "id": "app.viewer.copy", + "type": "button", + "title": "APP.ACTIONS.COPY", + "icon": "content_copy", + "actions": { + "click": "COPY_NODES" + }, + "rules": { + "visible": "app.toolbar.canCopyNode" + } + }, + { + "id": "app.viewer.move", + "type": "button", + "title": "APP.ACTIONS.MOVE", + "icon": "library_books", + "actions": { + "click": "MOVE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.viewer.delete", + "type": "button", + "title": "APP.ACTIONS.DELETE", + "icon": "delete", + "actions": { + "click": "DELETE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.viewer.versions", + "type": "button", + "title": "APP.ACTIONS.VERSIONS", + "icon": "history", + "actions": { + "click": "MANAGE_VERSIONS" + }, + "rules": { + "visible": "app.toolbar.versions" + } + }, + { + "id": "app.viewer.permissions", + "type": "button", + "title": "APP.ACTIONS.PERMISSIONS", + "icon": "settings_input_component", + "actions": { + "click": "MANAGE_PERMISSIONS" + }, + "rules": { + "visible": "app.toolbar.permissions" + } } ] } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7eb4713219..adacab4e36 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -26,11 +26,11 @@ }, "TOOLTIPS": { "CREATE_FOLDER": "Create new folder", - "CREATE_FOLDER_NOT_ALLOWED": "You can't create a folder here. You might not have the required permissions, check with your IT Team.", + "CREATE_FOLDER_NOT_ALLOWED": "Folders cannot be created whilst viewing the current items.", "UPLOAD_FILES": "Select files to upload", - "UPLOAD_FILES_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team.", + "UPLOAD_FILES_NOT_ALLOWED": "Files cannot be uploaded whilst viewing the current items", "UPLOAD_FOLDERS": "Select folders to upload", - "UPLOAD_FOLDERS_NOT_ALLOWED": "You need permissions to upload here, check with your IT Team." + "UPLOAD_FOLDERS_NOT_ALLOWED": "Folders cannot be uploaded whilst viewing the current items" } }, "BROWSE": { diff --git a/src/assets/plugins/plugin1.json b/src/assets/plugins/plugin1.json index 5d7cc43014..a3587600cb 100644 --- a/src/assets/plugins/plugin1.json +++ b/src/assets/plugins/plugin1.json @@ -27,7 +27,7 @@ "openWith": [ { "id": "plugin1.viewer.openWith.action1", - "type": "default", + "type": "button", "icon": "build", "title": "Snackbar", "actions": { @@ -53,6 +53,52 @@ "content": { "actions": [ { + "disabled": true, + "id": "app.toolbar.createFolder", + "type": "button", + "order": 10, + "title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER", + "description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER", + "icon": "create_new_folder", + "actions": { + "click": "CREATE_FOLDER" + }, + "rules": { + "visible": "app.navigation.folder.canCreate" + } + }, + { + "disabled": true, + "id": "app.toolbar.uploadFile", + "order": 11, + "type": "button", + "icon": "file_upload", + "title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE", + "description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES", + "actions": { + "click": "UPLOAD_FILES" + }, + "rules": { + "visible": "app.navigation.folder.canUpload" + } + }, + { + "disabled": true, + "id": "app.toolbar.uploadFolder", + "order": 12, + "type": "button", + "icon": "cloud_upload", + "title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER", + "description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS", + "actions": { + "click": "UPLOAD_FOLDER" + }, + "rules": { + "visible": "app.navigation.folder.canUpload" + } + }, + { + "disabled": true, "id": "plugin1.toolbar.menu1", "type": "menu", "icon": "storage", @@ -70,6 +116,7 @@ ] }, { + "disabled": true, "id": "plugin1.toolbar.separator3", "order": 301, "type": "separator" From a419726cf2c33f5e25ef4931567be2d47252c586 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 1 Aug 2018 12:06:20 +0100 Subject: [PATCH 063/146] support rule negation (#541) --- src/app/extensions/evaluators/core.evaluators.ts | 6 +++--- src/app/extensions/extension.service.ts | 16 +++++++++++++--- src/app/extensions/rule.extensions.ts | 3 ++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/extensions/evaluators/core.evaluators.ts b/src/app/extensions/evaluators/core.evaluators.ts index cd0f88d991..f943c64b7a 100644 --- a/src/app/extensions/evaluators/core.evaluators.ts +++ b/src/app/extensions/evaluators/core.evaluators.ts @@ -32,7 +32,7 @@ export function not(context: RuleContext, ...args: RuleParameter[]): boolean { return args .every(arg => { - const evaluator = context.evaluators[arg.value]; + const evaluator = context.getEvaluator(arg.value); if (!evaluator) { console.warn('evaluator not found: ' + arg.value); } @@ -47,7 +47,7 @@ export function every(context: RuleContext, ...args: RuleParameter[]): boolean { return args .every(arg => { - const evaluator = context.evaluators[arg.value]; + const evaluator = context.getEvaluator(arg.value); if (!evaluator) { console.warn('evaluator not found: ' + arg.value); } @@ -62,7 +62,7 @@ export function some(context: RuleContext, ...args: RuleParameter[]): boolean { return args .some(arg => { - const evaluator = context.evaluators[arg.value]; + const evaluator = context.getEvaluator(arg.value); if (!evaluator) { console.warn('evaluator not found: ' + arg.value); } diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 81f6772b8e..3b00abf3e8 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -33,7 +33,7 @@ import { NavigationState } from '../store/states/navigation.state'; import { selectionWithFolder } from '../store/selectors/app.selectors'; import { NavBarGroupRef } from './navbar.extensions'; import { RouteRef } from './routing.extensions'; -import { RuleContext, RuleRef, RuleEvaluator } from './rule.extensions'; +import { RuleContext, RuleRef, RuleEvaluator, RuleParameter } from './rule.extensions'; import { ActionRef, ContentActionRef, ContentActionType } from './action.extensions'; import * as core from './evaluators/core.evaluators'; import { NodePermissionService } from '../services/node-permission.service'; @@ -440,16 +440,26 @@ export class ExtensionService implements RuleContext { return value; } + getEvaluator(key: string): RuleEvaluator { + if (key && key.startsWith('!')) { + const fn = this.evaluators[key.substring(1)]; + return (context: RuleContext, ...args: RuleParameter[]): boolean => { + return !fn(context, ...args); + }; + } + return this.evaluators[key]; + } + evaluateRule(ruleId: string): boolean { const ruleRef = this.rules.find(ref => ref.id === ruleId); if (ruleRef) { - const evaluator = this.evaluators[ruleRef.type]; + const evaluator = this.getEvaluator(ruleRef.type); if (evaluator) { return evaluator(this, ...ruleRef.parameters); } } else { - const evaluator = this.evaluators[ruleId]; + const evaluator = this.getEvaluator(ruleId); if (evaluator) { return evaluator(this); } diff --git a/src/app/extensions/rule.extensions.ts b/src/app/extensions/rule.extensions.ts index bbab0c3e29..c06e1d6c21 100644 --- a/src/app/extensions/rule.extensions.ts +++ b/src/app/extensions/rule.extensions.ts @@ -32,8 +32,9 @@ export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean; export interface RuleContext { selection: SelectionState; navigation: NavigationState; - evaluators: { [key: string]: RuleEvaluator }; permissions: NodePermissions; + + getEvaluator(key: string): RuleEvaluator; } export class RuleRef { From 65777185b44998f53dc561e3b49c0b9d25c27c9f Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Thu, 2 Aug 2018 21:43:07 +0300 Subject: [PATCH 064/146] changed facet category name (#545) --- src/app.config.json | 2 +- src/assets/i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.config.json b/src/app.config.json index a49196e189..5e5ec7c31c 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -188,7 +188,7 @@ { "field": "content.mimetype", "mincount": 1, "label": "SEARCH.FACET_FIELDS.FILE_TYPE" }, { "field": "creator", "mincount": 1, "label": "SEARCH.FACET_FIELDS.CREATOR" }, { "field": "modifier", "mincount": 1, "label": "SEARCH.FACET_FIELDS.MODIFIER" }, - { "field": "SITE", "mincount": 1, "label": "SEARCH.FACET_FIELDS.FILE_LIBRARY" } + { "field": "SITE", "mincount": 1, "label": "SEARCH.FACET_FIELDS.LOCATION" } ], "facetQueries": { "label": "SEARCH.CATEGORIES.MODIFIED_DATE", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index adacab4e36..1d1274d915 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -312,7 +312,7 @@ "FILE_TYPE": "File Type", "CREATOR": "Creator", "MODIFIER": "Modifier", - "FILE_LIBRARY": "File Library" + "LOCATION": "Location" }, "CATEGORIES": { "MODIFIED_DATE": "Modified date", From ad6e027e6dcb5158e8294253d7e3ac0780894b91 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 3 Aug 2018 08:30:00 +0300 Subject: [PATCH 065/146] dont render title if its the same with name (#546) --- .../search-results-row/search-results-row.component.html | 2 +- .../search/search-results-row/search-results-row.component.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/components/search/search-results-row/search-results-row.component.html b/src/app/components/search/search-results-row/search-results-row.component.html index 8fc3fe23c0..b81e3b48ac 100644 --- a/src/app/components/search/search-results-row/search-results-row.component.html +++ b/src/app/components/search/search-results-row/search-results-row.component.html @@ -1,7 +1,7 @@
{{ name }} {{ name }} - ( {{ title }} ) + ( {{ title }} )
{{ description }}
diff --git a/src/app/components/search/search-results-row/search-results-row.component.ts b/src/app/components/search/search-results-row/search-results-row.component.ts index 2dd2dde48b..8c346b8028 100644 --- a/src/app/components/search/search-results-row/search-results-row.component.ts +++ b/src/app/components/search/search-results-row/search-results-row.component.ts @@ -81,6 +81,10 @@ export class SearchResultsRowComponent implements OnInit { return this.title; } + get showTitle() { + return this.name !== this.title; + } + get hasSize() { return this.size; } From 50b0023967d39fdbb582bafb7bb990bce5dd05c3 Mon Sep 17 00:00:00 2001 From: Adina Parpalita Date: Fri, 3 Aug 2018 13:33:27 +0300 Subject: [PATCH 066/146] [ACA-1638] refactor sharedLinksApi to use alfresco-js-api (#547) * refactor sharedLinksApi to use alfresco-js-api * update package-lock.json * re-created package-lock.json file * update package-lock * spellcheck fixes --- cspell.json | 1 + .../repo-client/apis/repo-api-new.ts | 46 + .../apis/shared-links/shared-links-api.ts | 67 +- e2e/utilities/repo-client/repo-client.ts | 36 +- package-lock.json | 1994 +++++++++-------- package.json | 1 + 6 files changed, 1196 insertions(+), 949 deletions(-) create mode 100644 e2e/utilities/repo-client/apis/repo-api-new.ts diff --git a/cspell.json b/cspell.json index aee8e87205..715d8afdb5 100644 --- a/cspell.json +++ b/cspell.json @@ -3,6 +3,7 @@ "language": "en", "words": [ "succes", + "sharedlinks", "ngrx", "ngstack", diff --git a/e2e/utilities/repo-client/apis/repo-api-new.ts b/e2e/utilities/repo-client/apis/repo-api-new.ts new file mode 100644 index 0000000000..362edb4b15 --- /dev/null +++ b/e2e/utilities/repo-client/apis/repo-api-new.ts @@ -0,0 +1,46 @@ +/*! + * @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 * as AlfrescoApi from 'alfresco-js-api-node'; +import { REPO_API_HOST } from '../../../configs'; +import { RepoClientAuth } from '../repo-client-models'; + +export abstract class RepoApiNew { + + alfrescoJsApi = new AlfrescoApi({ + provider: 'ECM', + hostEcm: REPO_API_HOST + }); + + constructor( + private username: string = RepoClientAuth.DEFAULT_USERNAME, + private password: string = RepoClientAuth.DEFAULT_PASSWORD + ) {} + + apiAuth() { + return this.alfrescoJsApi.login(this.username, this.password); + } + +} diff --git a/e2e/utilities/repo-client/apis/shared-links/shared-links-api.ts b/e2e/utilities/repo-client/apis/shared-links/shared-links-api.ts index 6140dcd74f..b7595e2941 100755 --- a/e2e/utilities/repo-client/apis/shared-links/shared-links-api.ts +++ b/e2e/utilities/repo-client/apis/shared-links/shared-links-api.ts @@ -23,55 +23,52 @@ * along with Alfresco. If not, see . */ -import { RepoApi } from '../repo-api'; -import { NodesApi } from '../nodes/nodes-api'; -import { RepoClient } from './../../repo-client'; +import { RepoApiNew } from '../repo-api-new'; import { Utils } from '../../../../utilities/utils'; -export class SharedLinksApi extends RepoApi { +export class SharedLinksApi extends RepoApiNew { - shareFileById(id: string): Promise { - const data = [{ nodeId: id }]; + constructor(username?, password?) { + super(username, password); + } - return this.post(`/shared-links`, { data }) - .catch(this.handleError); + async shareFileById(id: string) { + await this.apiAuth(); + const data = { nodeId: id }; + return await this.alfrescoJsApi.core.sharedlinksApi.addSharedLink(data); } - shareFilesByIds(ids: string[]): Promise { - return ids.reduce((previous, current) => ( - previous.then(() => this.shareFileById(current)) - ), Promise.resolve()); + async shareFilesByIds(ids: string[]) { + return await ids.reduce(async (previous: any, current: any) => { + await previous; + return await this.shareFileById(current); + }, Promise.resolve()); } - getSharedIdOfNode(name: string) { - return this.getSharedLinks() - .then(resp => resp.data.list.entries.find(entries => entries.entry.name === name)) - .then(resp => resp.entry.id) - .catch(this.handleError); + async getSharedIdOfNode(name: string) { + const sharedLinks = (await this.getSharedLinks()).list.entries; + const found = sharedLinks.find(sharedLink => sharedLink.entry.name === name); + return (found || { entry: { id: null } }).entry.id; } - unshareFile(name: string) { - return this.getSharedIdOfNode(name) - .then(id => this.delete(`/shared-links/${id}`)) - .catch(this.handleError); + async unshareFile(name: string) { + const id = await this.getSharedIdOfNode(name); + return await this.alfrescoJsApi.core.sharedlinksApi.deleteSharedLink(id); } - getSharedLinks(): Promise { - return this.get(`/shared-links`) - .catch(this.handleError); + async getSharedLinks() { + await this.apiAuth(); + return await this.alfrescoJsApi.core.sharedlinksApi.findSharedLinks(); } - waitForApi(data) { - const sharedFiles = () => { - return this.getSharedLinks() - .then(response => response.data.list.pagination.totalItems) - .then(totalItems => { - if ( totalItems < data.expect) { - return Promise.reject(totalItems); - } else { - return Promise.resolve(totalItems); - } - }); + async waitForApi(data) { + const sharedFiles = async () => { + const totalItems = (await this.getSharedLinks()).list.pagination.totalItems; + if ( totalItems < data.expect ) { + return Promise.reject(totalItems); + } else { + return Promise.resolve(totalItems); + } }; return Utils.retryCall(sharedFiles); diff --git a/e2e/utilities/repo-client/repo-client.ts b/e2e/utilities/repo-client/repo-client.ts index e3ffc0107d..153e55f703 100755 --- a/e2e/utilities/repo-client/repo-client.ts +++ b/e2e/utilities/repo-client/repo-client.ts @@ -34,14 +34,6 @@ import { TrashcanApi } from './apis/trashcan/trashcan-api'; import { SearchApi } from './apis/search/search-api'; export class RepoClient { - public people: PeopleApi = new PeopleApi(this.auth, this.config); - public nodes: NodesApi = new NodesApi(this.auth, this.config); - public sites: SitesApi = new SitesApi(this.auth, this.config); - public favorites: FavoritesApi = new FavoritesApi(this.auth, this.config); - public shared: SharedLinksApi = new SharedLinksApi(this.auth, this.config); - public trashcan: TrashcanApi = new TrashcanApi(this.auth, this.config); - public search: SearchApi = new SearchApi(this.auth, this.config); - constructor( private username: string = RepoClientAuth.DEFAULT_USERNAME, private password: string = RepoClientAuth.DEFAULT_PASSWORD, @@ -52,6 +44,34 @@ export class RepoClient { const { username, password } = this; return { username, password }; } + + get people () { + return new PeopleApi(this.auth, this.config); + } + + get nodes() { + return new NodesApi(this.auth, this.config); + } + + get sites() { + return new SitesApi(this.auth, this.config); + } + + get favorites() { + return new FavoritesApi(this.auth, this.config); + } + + get shared() { + return new SharedLinksApi(this.auth.username, this.auth.password); + } + + get trashcan() { + return new TrashcanApi(this.auth, this.config); + } + + get search() { + return new SearchApi(this.auth, this.config); + } } export * from './apis/nodes/node-body-create'; diff --git a/package-lock.json b/package-lock.json index 2f82082a16..c6f559c419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,16 +40,6 @@ "zone.js": "0.8.14" }, "dependencies": { - "alfresco-js-api": { - "version": "2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2", - "resolved": "https://registry.npmjs.org/alfresco-js-api/-/alfresco-js-api-2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2.tgz", - "integrity": "sha512-tcwfDzOHvgWchmpurmiRT6XwyojxZ02pr/fM+w36sU5sWyAlgsf2PNiOEzPaYCOC64FOOyNOK9XZOeQZkgs6Uw==", - "requires": { - "event-emitter": "0.3.4", - "jsrsasign": "^8.0.12", - "superagent": "3.8.2" - } - }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", @@ -105,16 +95,6 @@ "zone.js": "0.8.14" }, "dependencies": { - "alfresco-js-api": { - "version": "2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2", - "resolved": "https://registry.npmjs.org/alfresco-js-api/-/alfresco-js-api-2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2.tgz", - "integrity": "sha512-tcwfDzOHvgWchmpurmiRT6XwyojxZ02pr/fM+w36sU5sWyAlgsf2PNiOEzPaYCOC64FOOyNOK9XZOeQZkgs6Uw==", - "requires": { - "event-emitter": "0.3.4", - "jsrsasign": "^8.0.12", - "superagent": "3.8.2" - } - }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", @@ -179,10 +159,22 @@ "json-schema-traverse": "^0.3.0" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "dev": true, "requires": { "symbol-observable": "1.0.1" @@ -207,9 +199,9 @@ }, "dependencies": { "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "dev": true, "requires": { "symbol-observable": "1.0.1" @@ -306,15 +298,52 @@ "webpack-subresource-integrity": "^1.0.1" }, "dependencies": { + "chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "requires": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "dev": true, "requires": { "symbol-observable": "1.0.1" } }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + }, "symbol-observable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", @@ -487,6 +516,34 @@ "source-map": "^0.5.6", "tree-kill": "^1.0.0", "webpack-sources": "^1.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", + "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", + "dev": true, + "requires": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } } }, "@ngx-translate/core": { @@ -523,9 +580,9 @@ }, "dependencies": { "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "dev": true, "requires": { "symbol-observable": "1.0.1" @@ -540,9 +597,9 @@ } }, "@types/jasmine": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.7.tgz", - "integrity": "sha512-RdbrPcW1aD78UmdLiDa9ZCKrbR5Go8PXh6GCpb4oIOkWVEusubSJJDrP4c5RYOu8m/CBz+ygZpicj6Pgms5a4Q==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", + "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", "dev": true }, "@types/jasminewd2": { @@ -567,9 +624,9 @@ "dev": true }, "@types/selenium-webdriver": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.8.tgz", - "integrity": "sha512-yrqQvb1EZhH+ONMzUmsEnBjzitortVv0lynRe5US2+FofdoMWUE4wf7v4peCd62fqXq8COCVTbpS1/jIg5EbuQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.10.tgz", + "integrity": "sha512-ikB0JHv6vCR1KYUQAzTO4gi/lXLElT4Tx+6De2pc/OZwizE9LRNiTa+U8TBFKBD/nntPnr/MPSHSnOTybjhqNA==", "dev": true }, "@types/strip-bom": { @@ -601,9 +658,9 @@ } }, "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", "dev": true }, "acorn-dynamic-import": { @@ -643,23 +700,23 @@ "dev": true }, "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", "dev": true, "requires": { "es6-promisify": "^5.0.0" } }, "ajv": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", - "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", + "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", "requires": { - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0", - "uri-js": "^3.0.2" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.1" } }, "ajv-keywords": { @@ -677,6 +734,16 @@ "superagent": "3.8.2" } }, + "alfresco-js-api-node": { + "version": "2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2", + "resolved": "https://registry.npmjs.org/alfresco-js-api-node/-/alfresco-js-api-node-2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2.tgz", + "integrity": "sha512-73IzlNvJaoKoyuFYPao5B13XPoF3if0N3Xl08NUm4nO5qNa/540ditk0K/20ghVLd61D+E0tWw9lnuTLEISCdw==", + "requires": { + "event-emitter": "0.3.4", + "jsrsasign": "^8.0.12", + "superagent": "3.8.2" + } + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -758,12 +825,17 @@ }, "dependencies": { "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", "requires": { - "color-name": "^1.1.1" + "color-name": "1.1.1" } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" } } }, @@ -778,18 +850,18 @@ } }, "app-root-path": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", - "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz", + "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=", "dev": true }, "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", "dev": true, "requires": { - "default-require-extensions": "^1.0.0" + "default-require-extensions": "^2.0.0" } }, "aproba": { @@ -799,9 +871,9 @@ "dev": true }, "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "requires": { "delegates": "^1.0.0", @@ -907,10 +979,13 @@ "optional": true }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } }, "asn1.js": { "version": "4.10.1", @@ -930,6 +1005,23 @@ "dev": true, "requires": { "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } } }, "assert-plus": { @@ -945,19 +1037,19 @@ "dev": true }, "ast-types": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.3.tgz", - "integrity": "sha512-XA5o5dsNw8MhyW0Q7MWXJWc4oOzZKbdsEJq45h7c8q/d9DwWZ5F2ugUc1PuMLPGsUnphCt/cNDHu8JeBbxf1qA==", + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.5.tgz", + "integrity": "sha512-oJjo+5e7/vEc2FBK8gUalV0pba4L3VdBIs2EKhOLHLcOd2FgQIVQN9xb0eZ9IjEWyAL7vq6fGJxOvVvdCHNyMw==", "dev": true, "optional": true }, "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "dev": true, "requires": { - "lodash": "^4.14.0" + "lodash": "^4.17.10" } }, "async-each": { @@ -1283,9 +1375,9 @@ "dev": true }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, "optional": true, "requires": { @@ -1434,6 +1526,18 @@ "requires": { "ms": "2.0.0" } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true } } }, @@ -1518,14 +1622,15 @@ } }, "browserify-des": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", - "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dev": true, "requires": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", - "inherits": "^2.0.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "browserify-rsa": { @@ -1583,10 +1688,32 @@ "isarray": "^1.0.0" } }, - "buffer-from": { + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-fill": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, "buffer-indexof": { @@ -1741,9 +1868,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000833", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000833.tgz", - "integrity": "sha512-tKNuKu4WLImh4NxoTgntxFpDrRiA0Q6Q1NycNhuMST0Kx+Pt8YnRDW6V8xsyH6AtO2CpAoibatEk5eaEhP3O1g==", + "version": "1.0.30000874", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000874.tgz", + "integrity": "sha512-29nr1EPiHwrJTAHHsEmTt2h+55L8j2GNFdAcYPlRy2NX6iFz7ZZiepVI7kP/QqlnHLq3KvfWpbmGa0d063U09w==", "dev": true }, "caseless": { @@ -1763,14 +1890,13 @@ } }, "chalk": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.2.tgz", - "integrity": "sha512-LvixLAQ4MYhbf7hgL4o5PeK32gJKvVzDRiSNIApDofQvyhl8adgG2lJVXn4+ekQoK7HL9RF8lqxwerpe0x2pCw==", - "dev": true, + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "^3.1.0", + "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" + "supports-color": "^5.3.0" } }, "chart.js": { @@ -1839,9 +1965,9 @@ "dev": true }, "circular-json": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.4.tgz", - "integrity": "sha512-vnJA8KS0BfOihugYEUkLRcnmq21FbuivbxgzDLXNs3zIk4KllV4Mx4UuTzBXht9F00C7QfD1YqMXg1zP6EXpig==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz", + "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA==", "dev": true }, "class-utils": { @@ -1874,12 +2000,20 @@ } }, "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.0.tgz", + "integrity": "sha1-Cp1iBAzdx5BMXtrum97KZC2bnBw=", "dev": true, "requires": { - "source-map": "0.5.x" + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "cliui": { @@ -1894,9 +2028,9 @@ } }, "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "dev": true }, "clone-deep": { @@ -1974,34 +2108,11 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", "dev": true }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.x.x" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.x.x" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.x.x" - } - } - } + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true }, "har-schema": { "version": "2.0.0", @@ -2019,24 +2130,6 @@ "har-schema": "^2.0.0" } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.x.x", - "cryptiles": "3.x.x", - "hoek": "4.x.x", - "sntp": "2.x.x" - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", - "dev": true - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2048,6 +2141,12 @@ "sshpk": "^1.7.0" } }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2055,9 +2154,9 @@ "dev": true }, "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -2068,7 +2167,6 @@ "forever-agent": "~0.6.1", "form-data": "~2.3.1", "har-validator": "~5.0.3", - "hawk": "~6.0.2", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -2078,20 +2176,10 @@ "performance-now": "^2.1.0", "qs": "~6.5.1", "safe-buffer": "^5.1.1", - "stringstream": "~0.0.5", "tough-cookie": "~2.3.3", "tunnel-agent": "^0.6.0", "uuid": "^3.1.0" } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.x.x" - } } } }, @@ -2102,9 +2190,9 @@ "dev": true }, "codelyzer": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.3.0.tgz", - "integrity": "sha512-RLMrtLwrBS0dfo2/KTP+2NHofCpzcuh0bEp/A/naqvQonbUL4AW/qWQdbpn8dMNudtpmzEx9eS8KEpGdVPg1BA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.4.2.tgz", + "integrity": "sha512-tW796ECKMAynFtl/yyS5NRYhufbT3CEKjjMQ450kUeCcQlK7OIqD9VGRVwC3gSQSK4VaewCKCaVL0bzv9PhsLg==", "dev": true, "requires": { "app-root-path": "^2.0.1", @@ -2112,7 +2200,15 @@ "cssauron": "^1.4.0", "semver-dsl": "^1.0.1", "source-map": "^0.5.7", - "sprintf-js": "^1.0.3" + "sprintf-js": "^1.1.1" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", + "dev": true + } } }, "collection-visit": { @@ -2159,9 +2255,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==" }, "comment-json": { "version": "1.1.3", @@ -2172,13 +2268,10 @@ } }, "common-tags": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.7.2.tgz", - "integrity": "sha512-joj9ZlUOjCrwdbmiLqafeUSgkUM74NqhLsZtSqDmhKudaIY197zTrb8JMl31fMnCUuxwFT23eC/oWvrZzDLRJQ==", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0" - } + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true }, "commondir": { "version": "1.0.1", @@ -2187,9 +2280,9 @@ "dev": true }, "compare-versions": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz", - "integrity": "sha512-4hAxDSBypT/yp2ySFD346So6Ragw5xmBn/e/agIGl3bZr6DLUqnoRZPusxKrXdYRZpgexO9daejmIenlq/wrIQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", "dev": true }, "component-bind": { @@ -2210,26 +2303,26 @@ "dev": true }, "compressible": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", - "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", + "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", "dev": true, "requires": { - "mime-db": ">= 1.33.0 < 2" + "mime-db": ">= 1.34.0 < 2" } }, "compression": { - "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", - "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", + "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", "dev": true, "requires": { - "accepts": "~1.3.4", + "accepts": "~1.3.5", "bytes": "3.0.0", - "compressible": "~2.0.13", + "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", - "safe-buffer": "5.1.1", + "safe-buffer": "5.1.2", "vary": "~1.1.2" }, "dependencies": { @@ -2241,12 +2334,6 @@ "requires": { "ms": "2.0.0" } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true } } }, @@ -2472,32 +2559,33 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", - "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", "dev": true, "requires": { "is-directory": "^0.3.1", - "js-yaml": "^3.4.3", - "minimist": "^1.2.0", - "object-assign": "^4.1.0", - "os-homedir": "^1.0.1", - "parse-json": "^2.2.0", - "require-from-string": "^1.1.0" + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" }, "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } } } }, "create-ecdh": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.1.tgz", - "integrity": "sha512-iZvCCg8XqHQZ1ioNBTzXS/cQSkqkqcPs8xSX4upNB+DAk9Ht3uzQf2J32uAHNCne8LDmKr29AgZrEs4oIrwLuQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "dev": true, "requires": { "bn.js": "^4.1.0", @@ -2605,71 +2693,33 @@ "xregexp": "^4.1.1" }, "dependencies": { - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "requires": { "symbol-observable": "1.0.1" } }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, "symbol-observable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, - "xregexp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.1.1.tgz", - "integrity": "sha512-QJ1gfSUV7kEOLfpKFCjBJRnfPErUzkNKFMso4kDSmGpp3x6ZgkyKf74inxI7PnnQCFYq5TqYJCd7DrgDN8Q05A==" } } }, "cspell-dict-cpp": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/cspell-dict-cpp/-/cspell-dict-cpp-1.1.6.tgz", - "integrity": "sha512-eVNenrvoViBsrfZYzChoJ1YJ5b0VxwgYCFhxxKjL3Bjk3Te98FM8Bk/ExSnv5KlwT56EhT5y+CBi0ejEv5XW/g==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/cspell-dict-cpp/-/cspell-dict-cpp-1.1.8.tgz", + "integrity": "sha512-l6/Lc0bCauWuMm/kxjOfS39QuMi8wDIU9c3xeCbbWGNc9rV81VeHRZpCkp+CIfjtTAfIcrn3ZJNzL41oMxTArQ==", "requires": { "configstore": "^3.1.0" } }, "cspell-dict-django": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cspell-dict-django/-/cspell-dict-django-1.0.2.tgz", - "integrity": "sha512-t52Ga2S7GgsCYaLsN+iqWLgHUHfqGfkMUv0gSjp2QOVOxGNQ4kjj1oJ6GkcZB5k6UnI2sgQ7uku/bjmNlnctDw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cspell-dict-django/-/cspell-dict-django-1.0.4.tgz", + "integrity": "sha512-UhD1fzgKOLGXZRRB+WjNwqLtWutcXJD1kHzT5ZC3IS613rPlWeK0N4q790Y9IjVk2S1sh3VYcJ/gStHS/CTLqA==", "requires": { "configstore": "^3.1.0" } @@ -2741,9 +2791,9 @@ }, "dependencies": { "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", "requires": { "symbol-observable": "1.0.1" } @@ -2767,6 +2817,18 @@ "rxjs": "^5.5.2", "rxjs-from-iterable": "^1.0.5", "rxjs-stream": "^1.0.4" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } } }, "css-parse": { @@ -2924,12 +2986,20 @@ "optional": true }, "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", "dev": true, "requires": { - "strip-bom": "^2.0.0" + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } } }, "define-properties": { @@ -3259,9 +3329,9 @@ "optional": true }, "duplexify": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz", - "integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", "dev": true, "requires": { "end-of-stream": "^1.0.0", @@ -3271,13 +3341,14 @@ } }, "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "dev": true, "optional": true, "requires": { - "jsbn": "~0.1.0" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "ee-first": { @@ -3287,15 +3358,15 @@ "dev": true }, "ejs": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", - "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", "dev": true }, "electron-to-chromium": { - "version": "1.3.45", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.45.tgz", - "integrity": "sha1-RYrBscXHYM6IEaFtK/vZfsMLr7g=", + "version": "1.3.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.55.tgz", + "integrity": "sha1-8VDhCyC3fZ1Br8yjEu/gw7Gn/c4=", "dev": true }, "elliptic": { @@ -3420,18 +3491,18 @@ } }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { "is-arrayish": "^0.2.1" } }, "es-abstract": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", - "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", "dev": true, "requires": { "es-to-primitive": "^1.1.1", @@ -3453,9 +3524,9 @@ } }, "es5-ext": { - "version": "0.10.42", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", - "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", "requires": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.1", @@ -3518,9 +3589,9 @@ } }, "es6-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", - "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", "dev": true }, "es6-promisify": { @@ -3530,14 +3601,6 @@ "dev": true, "requires": { "es6-promise": "^4.0.3" - }, - "dependencies": { - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", - "dev": true - } } }, "es6-set": { @@ -3628,9 +3691,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", "dev": true, "optional": true, "requires": { @@ -3670,10 +3733,9 @@ } }, "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" }, "esrecurse": { "version": "4.2.1", @@ -3896,6 +3958,12 @@ "ms": "2.0.0" } }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", @@ -3905,9 +3973,9 @@ } }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -3963,6 +4031,18 @@ "json-schema-traverse": "^0.3.0" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "schema-utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", @@ -3981,9 +4061,9 @@ "dev": true }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -4046,14 +4126,14 @@ } }, "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", "dev": true, "requires": { "is-number": "^2.1.0", "isobject": "^2.0.0", - "randomatic": "^1.1.3", + "randomatic": "^3.0.0", "repeat-element": "^1.1.2", "repeat-string": "^1.5.2" } @@ -4115,9 +4195,9 @@ } }, "follow-redirects": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", - "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.2.tgz", + "integrity": "sha512-kssLorP/9acIdpQ2udQVTiCS5LQmdEz9mvdIfDcl1gYX2tPKFADHSyFdvJS040XdFsPzemWtgI3q8mFVCxtX8A==", "dev": true, "requires": { "debug": "^3.1.0" @@ -4206,9 +4286,9 @@ } }, "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -4233,14 +4313,14 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.3.tgz", - "integrity": "sha512-X+57O5YkDTiEQGiw8i7wYc2nQgweIekqkepI8Q3y4wVlurgBt2SuwxTeYUYMZIGpLZH3r/TsMjczCMXE5ZOt7Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "dev": true, "optional": true, "requires": { "nan": "^2.9.2", - "node-pre-gyp": "^0.9.0" + "node-pre-gyp": "^0.10.0" }, "dependencies": { "abbrev": { @@ -4321,7 +4401,7 @@ } }, "deep-extend": { - "version": "0.4.2", + "version": "0.5.1", "bundled": true, "dev": true, "optional": true @@ -4499,7 +4579,7 @@ } }, "node-pre-gyp": { - "version": "0.9.1", + "version": "0.10.0", "bundled": true, "dev": true, "optional": true, @@ -4608,12 +4688,12 @@ "optional": true }, "rc": { - "version": "1.2.6", + "version": "1.2.7", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "~0.4.0", + "deep-extend": "^0.5.1", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -4810,6 +4890,13 @@ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true, "optional": true + }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "dev": true, + "optional": true } } }, @@ -4836,9 +4923,9 @@ } }, "gaze": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", - "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "dev": true, "optional": true, "requires": { @@ -4868,9 +4955,9 @@ "integrity": "sha512-AyZrG5Qq8Tn0qnaDCnH2n9TsWnJLKBXEa2FcUlHWfEgl1rRS3MbcvB4OsarxyEekx/PwYlyKXvjQwNvYsByXAw==" }, "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, "get-stdin": { @@ -4886,9 +4973,9 @@ "dev": true }, "get-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.1.tgz", - "integrity": "sha512-7aelVrYqCLuVjq2kEKRTH8fXPTC0xKTkM+G7UlFkEwCXY3sFbSxvY375JoFowOAYbkaU47SrBvOefUlLZZ+6QA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz", + "integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==", "dev": true, "optional": true, "requires": { @@ -4988,14 +5075,14 @@ } }, "globule": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", - "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", "dev": true, "optional": true, "requires": { "glob": "~7.1.1", - "lodash": "~4.17.4", + "lodash": "~4.17.10", "minimatch": "~3.0.2" } }, @@ -5126,12 +5213,12 @@ } }, "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "^1.0.2" + "function-bind": "^1.1.1" } }, "has-ansi": { @@ -5144,9 +5231,9 @@ } }, "has-binary2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", - "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", "dev": true, "requires": { "isarray": "2.0.1" @@ -5167,10 +5254,9 @@ "dev": true }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-unicode": { "version": "2.0.1", @@ -5249,13 +5335,13 @@ } }, "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", "dev": true, "requires": { "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.1" } }, "hawk": { @@ -5314,9 +5400,9 @@ } }, "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, "hpack.js": { @@ -5338,18 +5424,29 @@ "dev": true }, "html-minifier": { - "version": "3.5.15", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.15.tgz", - "integrity": "sha512-OZa4rfb6tZOZ3Z8Xf0jKxXkiDcFWldQePGYFDcgKqES2sXeWaEv9y6QQvWUtX3ySI3feApQi5uCsHLINQ6NoAw==", + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.19.tgz", + "integrity": "sha512-Qr2JC9nsjK8oCrEmuB430ZIA8YWbF3D5LSjywD75FTuXmeqacwHgIM8wp3vHYzzPbklSjp53RdmDuzR4ub2HzA==", "dev": true, "requires": { "camel-case": "3.0.x", "clean-css": "4.1.x", - "commander": "2.15.x", + "commander": "2.16.x", "he": "1.1.x", "param-case": "2.1.x", "relateurl": "0.2.x", - "uglify-js": "3.3.x" + "uglify-js": "3.4.x" + }, + "dependencies": { + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.x" + } + } } }, "html-webpack-plugin": { @@ -5446,9 +5543,9 @@ } }, "http-parser-js": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.12.tgz", - "integrity": "sha1-uc+/Sizybw/DSxDKFImid3HjR08=", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", "dev": true }, "http-proxy": { @@ -5545,14 +5642,17 @@ } }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "ieee754": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", - "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", "dev": true }, "iferr": { @@ -5562,9 +5662,9 @@ "dev": true }, "ignore": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", - "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==", + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, "image-size": { @@ -5580,6 +5680,24 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -5681,9 +5799,9 @@ "dev": true }, "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", "dev": true }, "is-accessor-descriptor": { @@ -5726,9 +5844,9 @@ } }, "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", "dev": true }, "is-data-descriptor": { @@ -5860,23 +5978,6 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -5988,15 +6089,18 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isbinaryfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", - "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } }, "isemail": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.2.tgz", - "integrity": "sha512-zfRhJn9rFSGhzU5tGZqepRSAj3+g6oTOHxMGGriWNJZzyLPUK8H7VHpqKntegnW8KLyGA9zwuNaCoopl40LTpg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.3.tgz", + "integrity": "sha512-5xbsG5wYADIcB+mfLsd+nst1V/D+I7EU7LEZPo2GOIMu4JzfcRs5yQoypP4avA7QtUqgxYLKBYNv4IdzBmbhdw==", "dev": true, "requires": { "punycode": "2.x.x" @@ -6067,6 +6171,18 @@ "json-schema-traverse": "^0.3.0" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "schema-utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", @@ -6085,12 +6201,12 @@ "dev": true }, "istanbul-lib-hook": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", - "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", "dev": true, "requires": { - "append-transform": "^0.4.0" + "append-transform": "^1.0.0" } }, "istanbul-lib-instrument": { @@ -6138,9 +6254,9 @@ } }, "istanbul-lib-source-maps": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", - "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", "dev": true, "requires": { "debug": "^3.1.0", @@ -6183,9 +6299,9 @@ "dev": true }, "jasmine-reporters": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-2.3.1.tgz", - "integrity": "sha1-9C1XjplmlhY0MdkRwxZ5cZ+0Ozs=", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-2.3.2.tgz", + "integrity": "sha512-u/7AT9SkuZsUfFBLLzbErohTGNsEUCKaQbsVYnLFW1gEuL2DzmBL4n8v90uZsqIqlWvWUgian8J6yOt5Fyk/+A==", "dev": true, "requires": { "mkdirp": "^0.5.1", @@ -6262,9 +6378,9 @@ } }, "js-base64": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", - "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.8.tgz", + "integrity": "sha512-hm2nYpDrwoO/OzBhdcqs/XGT6XjSuSSCVEpia+Kl2J6x4CYt5hISlVL/AYU1khoDXv0AQVgxtdJySb9gjAn56Q==", "dev": true, "optional": true }, @@ -6275,13 +6391,21 @@ "dev": true }, "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } } }, "jsbn": { @@ -6303,19 +6427,18 @@ "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-parser": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/json-parser/-/json-parser-1.1.5.tgz", "integrity": "sha1-5i7FJh0aal/CDoEqMgdAxtkAVnc=", "requires": { "esprima": "^2.7.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - } } }, "json-schema": { @@ -6325,9 +6448,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify": { "version": "1.0.1", @@ -6420,6 +6543,12 @@ "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", "dev": true }, + "es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -6511,12 +6640,12 @@ } }, "karma-coverage-istanbul-reporter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.2.tgz", - "integrity": "sha512-sQHexslLF+QHzaKfK8+onTYMyvSwv+p5cDayVxhpEELGa3z0QuB+l0IMsicIkkBNMOJKQaqueiRoW7iuo7lsog==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.3.tgz", + "integrity": "sha1-O13/RmT6W41RlrmInj9hwforgNk=", "dev": true, "requires": { - "istanbul-api": "^1.1.14", + "istanbul-api": "^1.3.1", "minimatch": "^3.0.4" } }, @@ -6536,12 +6665,30 @@ } }, "karma-source-map-support": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.2.0.tgz", - "integrity": "sha1-G/gee7SwiWJ6s1LsQXnhF8QGpUA=", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz", + "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==", "dev": true, "requires": { - "source-map-support": "^0.4.1" + "source-map-support": "^0.5.5" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } } }, "killable": { @@ -6742,6 +6889,12 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, "lodash.mergewith": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", @@ -6762,9 +6915,9 @@ "dev": true }, "log4js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.6.0.tgz", - "integrity": "sha512-9rG2W9o0D4GJDzQjno1rRpe+hzK0IEG/uGdjzNROStW/DWhV3sNX2r8OdPKppThlK7gr+08C5FSReWqmaRb/Ww==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.11.0.tgz", + "integrity": "sha512-z1XdwyGFg8/WGkOyF6DPJjivCWNLKrklGdViywdYnSKOvgtEBo2UyEMZS5sD2mZrQlU3TvO8wDWLc8mzE1ncBQ==", "dev": true, "requires": { "amqplib": "^0.5.2", @@ -6920,12 +7073,12 @@ "dev": true }, "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "requires": { - "js-tokens": "^3.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" } }, "loud-rejection": { @@ -6945,9 +7098,9 @@ "dev": true }, "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", "dev": true, "requires": { "pseudomap": "^1.0.2", @@ -6975,9 +7128,9 @@ } }, "mailgun-js": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.18.0.tgz", - "integrity": "sha512-o0P6jjZlx5CQj12tvVgDTbgjTqVN0+5h6/6P1+3c6xmozVKBwniQ6Qt3MkCSF0+ueVTbobAfWyGpWRZMJu8t1g==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.18.1.tgz", + "integrity": "sha512-lvuMP14u24HS2uBsJEnzSyPMxzU2b99tQsIx1o6QNjqxjk8b3WvR+vq5oG1mjqz/IBYo+5gF+uSoDS0RkMVHmg==", "dev": true, "optional": true, "requires": { @@ -6993,9 +7146,9 @@ } }, "make-dir": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", - "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "requires": { "pify": "^3.0.0" } @@ -7027,6 +7180,12 @@ "object-visit": "^1.0.0" } }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -7136,16 +7295,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", "requires": { - "mime-db": "~1.33.0" + "mime-db": "~1.35.0" } }, "mimic-fn": { @@ -7302,9 +7461,9 @@ "optional": true }, "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -7312,7 +7471,6 @@ "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "fragment-cache": "^0.2.1", - "is-odd": "^2.0.0", "is-windows": "^1.0.2", "kind-of": "^6.0.2", "object.pick": "^1.3.0", @@ -7399,27 +7557,26 @@ "integrity": "sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=" }, "node-forge": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", "dev": true }, "node-gyp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", - "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz", + "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==", "dev": true, "optional": true, "requires": { "fstream": "^1.0.0", "glob": "^7.0.3", "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", "mkdirp": "^0.5.0", "nopt": "2 || 3", "npmlog": "0 || 1 || 2 || 3 || 4", "osenv": "0", - "request": "2", + "request": ">=2.9.0 <2.82.0", "rimraf": "2", "semver": "~5.3.0", "tar": "^2.0.0", @@ -7519,9 +7676,9 @@ } }, "node-sass": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", - "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", + "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", "dev": true, "optional": true, "requires": { @@ -7540,22 +7697,43 @@ "nan": "^2.10.0", "node-gyp": "^3.3.1", "npmlog": "^4.0.0", - "request": "~2.79.0", + "request": "2.87.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" }, "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "optional": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "dev": true, + "optional": true }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", "dev": true, "optional": true }, @@ -7564,6 +7742,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, + "optional": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -7572,77 +7751,90 @@ "supports-color": "^2.0.0" } }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true, + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "dev": true, "optional": true, "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" + "ajv": "^5.1.0", + "har-schema": "^2.0.0" } }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dev": true, "optional": true, "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true, + "optional": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true, "optional": true }, "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "dev": true, "optional": true, "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", "combined-stream": "~1.0.5", - "extend": "~3.0.0", + "extend": "~3.0.1", "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" } }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", "dev": true, "optional": true } @@ -7859,9 +8051,9 @@ } }, "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", "dev": true }, "object-visit": { @@ -7987,24 +8179,12 @@ "dev": true }, "original": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.0.tgz", - "integrity": "sha1-kUf5P6FpbQS+YeAb1QuurKZWvTs=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz", + "integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==", "dev": true, "requires": { - "url-parse": "1.0.x" - }, - "dependencies": { - "url-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.0.5.tgz", - "integrity": "sha1-CFSGBCKv3P7+tsllxmLUgAFpkns=", - "dev": true, - "requires": { - "querystringify": "0.0.x", - "requires-port": "1.0.x" - } - } + "url-parse": "~1.4.0" } }, "os-browserify": { @@ -8051,9 +8231,9 @@ "dev": true }, "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { "p-try": "^1.0.0" @@ -8095,6 +8275,19 @@ "pac-resolver": "^3.0.0", "raw-body": "^2.2.0", "socks-proxy-agent": "^3.0.0" + }, + "dependencies": { + "socks-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz", + "integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==", + "dev": true, + "optional": true, + "requires": { + "agent-base": "^4.1.0", + "socks": "^1.1.10" + } + } } }, "pac-resolver": { @@ -8374,9 +8567,9 @@ "dev": true }, "postcss": { - "version": "6.0.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz", - "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==", + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", "dev": true, "requires": { "chalk": "^2.4.1", @@ -8384,37 +8577,11 @@ "supports-color": "^5.4.0" }, "dependencies": { - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -8431,46 +8598,24 @@ } }, "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0", - "postcss-load-options": "^1.2.0", - "postcss-load-plugins": "^2.3.0" - } - }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0" - } - }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", "dev": true, "requires": { - "cosmiconfig": "^2.1.1", - "object-assign": "^4.1.0" + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" } }, "postcss-loader": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.4.tgz", - "integrity": "sha512-L2p654oK945B/gDFUGgOhh7uzj19RWoY1SVMeJVoKno1H2MdbQ0RppR/28JGju4pMb22iRC7BJ9aDzbxXSLf4A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.6.tgz", + "integrity": "sha512-hgiWSc13xVQAq25cVw80CH0l49ZKlAnU1hKPOdRrNj89bokRr/bZF2nT+hebPPF9c9xs8c3gw3Fr2nxtmXYnNg==", "dev": true, "requires": { "loader-utils": "^1.1.0", "postcss": "^6.0.0", - "postcss-load-config": "^1.2.0", + "postcss-load-config": "^2.0.0", "schema-utils": "^0.4.0" } }, @@ -8576,9 +8721,9 @@ }, "dependencies": { "@types/node": { - "version": "6.0.110", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz", - "integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==", + "version": "6.0.115", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.115.tgz", + "integrity": "sha512-PWA07jqflLli+PAk7VaJn0MVdTw96egk5B1FxwocV/tcc3RamNGbza1ZgS0OGUsTuAYCFCboL+IlG2bPazV2Nw==", "dev": true }, "@types/selenium-webdriver": { @@ -8593,12 +8738,36 @@ "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", "dev": true }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -8627,6 +8796,12 @@ "rimraf": "^2.2.8" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, "globby": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", @@ -8641,12 +8816,51 @@ "pinkie-promise": "^2.0.0" } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -8659,6 +8873,34 @@ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", "dev": true }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, "selenium-webdriver": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", @@ -8687,19 +8929,19 @@ } }, "webdriver-manager": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", - "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.0.tgz", + "integrity": "sha512-oEc5fmkpz6Yh6udhwir5m0eN5mgRPq9P/NU5YWuT3Up5slt6Zz+znhLU7q4+8rwCZz/Qq3Fgpr/4oao7NPCm2A==", "dev": true, "requires": { - "adm-zip": "^0.4.7", + "adm-zip": "^0.4.9", "chalk": "^1.1.1", "del": "^2.2.0", "glob": "^7.0.3", "ini": "^1.3.4", "minimist": "^1.2.0", "q": "^1.4.1", - "request": "^2.78.0", + "request": "^2.87.0", "rimraf": "^2.5.2", "semver": "^5.3.0", "xml2js": "^0.4.17" @@ -8708,19 +8950,19 @@ } }, "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", "dev": true, "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.6.0" + "ipaddr.js": "1.8.0" } }, "proxy-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.0.tgz", - "integrity": "sha512-g6n6vnk8fRf705ShN+FEXFG/SDJaW++lSs0d9KaJh4uBWW/wi7en4Cpo5VYQW3SZzAE121lhB/KLQrbURoubZw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.1.tgz", + "integrity": "sha512-mAZexaz9ZxQhYPWfAjzlrloEjW+JHiBFryE4AJXFDTnaXfmH/FKqC1swTRKuEPbHWz02flQNXFOyDUF7zfEG6A==", "dev": true, "optional": true, "requires": { @@ -8731,7 +8973,7 @@ "lru-cache": "^4.1.2", "pac-proxy-agent": "^2.0.1", "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^3.0.0" + "socks-proxy-agent": "^4.0.1" } }, "proxy-from-env": { @@ -8777,20 +9019,20 @@ } }, "pumpify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", - "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "duplexify": "^3.5.3", + "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" } }, "punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.5.1", @@ -8805,9 +9047,9 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "querystring": { "version": "0.2.0", @@ -8822,49 +9064,33 @@ "dev": true }, "querystringify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz", - "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", + "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", "dev": true }, "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", "dev": true, "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" }, "dependencies": { "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true }, "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -8931,6 +9157,12 @@ "statuses": ">= 1.3.1 < 2" } }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, "setprototypeof": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", @@ -9091,9 +9323,9 @@ "integrity": "sha1-tPg3BEFqytiZiMmxVjXUfgO5NEo=" }, "regenerate": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", - "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", "dev": true }, "regenerator-runtime": { @@ -9299,9 +9531,9 @@ "dev": true }, "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "require-main-filename": { @@ -9317,9 +9549,9 @@ "dev": true }, "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", "dev": true, "requires": { "path-parse": "^1.0.5" @@ -9427,6 +9659,11 @@ "ret": "~0.1.10" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", @@ -9460,27 +9697,6 @@ "dev": true, "requires": { "https-proxy-agent": "^2.2.1" - }, - "dependencies": { - "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "dev": true, - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - } - } } }, "sax": { @@ -9551,12 +9767,12 @@ } }, "selfsigned": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.2.tgz", - "integrity": "sha1-tESVgNmZKbZbEKSDiTAaZZIIh1g=", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", + "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==", "dev": true, "requires": { - "node-forge": "0.7.1" + "node-forge": "0.7.5" } }, "semver": { @@ -9801,7 +10017,8 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=", - "dev": true + "dev": true, + "optional": true }, "smtp-connection": { "version": "2.12.0", @@ -10070,19 +10287,41 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz", "integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=", "dev": true, + "optional": true, "requires": { "ip": "^1.1.4", "smart-buffer": "^1.0.13" } }, "socks-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz", - "integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", + "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", "dev": true, + "optional": true, "requires": { - "agent-base": "^4.1.0", - "socks": "^1.1.10" + "agent-base": "~4.2.0", + "socks": "~2.2.0" + }, + "dependencies": { + "smart-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", + "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==", + "dev": true, + "optional": true + }, + "socks": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.1.tgz", + "integrity": "sha512-0GabKw7n9mI46vcNrVfs0o6XzWzjVa3h6GaSo2UPxtWAROXUWavfJWh1M4PR5tnE0dcnQXZIDFP4yrAysLze/w==", + "dev": true, + "optional": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.0.1" + } + } } }, "source-list-map": { @@ -10098,12 +10337,12 @@ "dev": true }, "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "dev": true, "requires": { - "atob": "^2.0.0", + "atob": "^2.1.1", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -10224,9 +10463,9 @@ "dev": true }, "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "dev": true, "requires": { "asn1": "~0.2.3", @@ -10236,6 +10475,7 @@ "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "dependencies": { @@ -10310,9 +10550,9 @@ } }, "stream-each": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dev": true, "requires": { "end-of-stream": "^1.1.0", @@ -10320,14 +10560,14 @@ } }, "stream-http": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", - "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dev": true, "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", - "readable-stream": "^2.3.3", + "readable-stream": "^2.3.6", "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" } @@ -10370,9 +10610,9 @@ } }, "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", "dev": true }, "strip-ansi": { @@ -10436,6 +10676,18 @@ "json-schema-traverse": "^0.3.0" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "schema-utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", @@ -10523,12 +10775,11 @@ } }, "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "^2.0.0" + "has-flag": "^3.0.0" } }, "symbol-observable": { @@ -10592,9 +10843,9 @@ "dev": true }, "time-stamp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", - "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.1.tgz", + "integrity": "sha512-KUnkvOWC3C+pEbwE/0u3CcmNpGCDqkYGYZOphe1QFxApYQkJ5g195TDBjgZch/zG6chU1NcabLwnM7BCpWAzTQ==", "dev": true }, "timers-browserify": { @@ -10784,23 +11035,6 @@ "yn": "^2.0.0" }, "dependencies": { - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -10814,23 +11048,14 @@ "dev": true }, "source-map-support": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.5.tgz", - "integrity": "sha512-mR7/Nd5l1z6g99010shcXJiNEaf3fEtmLhRB/sBcQVJGodcHCULPp2y4Sfa43Kv2zq7T+Izmfp/WHCR6dYkQCA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -10875,9 +11100,9 @@ } }, "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { "version": "5.9.1", @@ -10897,47 +11122,19 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.12.1" - }, - "dependencies": { - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "tsscmp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", - "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "dev": true, "optional": true }, "tsutils": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.26.2.tgz", - "integrity": "sha512-uzwnhmrSbyinPCiwfzGsOY3IulBTwoky7r83HmZdz9QNCjhSCzavkh47KLWuU0zF2F2WbpmmzoJUIEiYyd+jEQ==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -10997,12 +11194,12 @@ "dev": true }, "uglify-js": { - "version": "3.3.23", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.23.tgz", - "integrity": "sha512-Ks+KqLGDsYn4z+pU7JsKCzC0T3mPYl+rU+VcPZiQOazjE4Uqi4UCRY3qPMDbJi7ze37n1lDXj3biz1ik93vqvw==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.6.tgz", + "integrity": "sha512-O1D7L6WcOzS1qW2ehopEm4cWm5yA6bQBozlks8jO8ODxYCy4zv+bR/la4Lwp01tpkYGNonnpXvUpYtrvSu8Yzg==", "dev": true, "requires": { - "commander": "~2.15.0", + "commander": "~2.16.0", "source-map": "~0.6.1" }, "dependencies": { @@ -11022,9 +11219,9 @@ "optional": true }, "uglifyjs-webpack-plugin": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz", - "integrity": "sha512-hIQJ1yxAPhEA2yW/i7Fr+SXZVMp+VEI3d42RTHBgQd2yhp/1UdBcR3QEWPV5ahBxlqQDMEMTuTEvDHSFINfwSw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", + "integrity": "sha512-1VicfKhCYHLS8m1DCApqBhoulnASsEoJ/BvpUpP4zoNAPpKzdH+ghk0olGJMmwX2/jprK2j3hAHdUbczBSy2FA==", "dev": true, "requires": { "cacache": "^10.0.4", @@ -11135,9 +11332,9 @@ } }, "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "unpipe": { "version": "1.0.0", @@ -11192,9 +11389,9 @@ } }, "upath": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.0.5.tgz", - "integrity": "sha512-qbKn90aDQ0YEwvXoLqj0oiuUYroLX2lVHZ+b+xwjozFasAOC4GneDq5+OaIG5Zj+jFmbz/uO+f7a9qxjktJQww==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, "upper-case": { @@ -11204,9 +11401,9 @@ "dev": true }, "uri-js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", - "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { "punycode": "^2.1.0" } @@ -11258,6 +11455,18 @@ "json-schema-traverse": "^0.3.0" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "schema-utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", @@ -11270,39 +11479,20 @@ } }, "url-parse": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.0.tgz", - "integrity": "sha512-ERuGxDiQ6Xw/agN4tuoCRbmwRuZP0cJ1lJxJubXr5Q/5cDa78+Dc4wfvtxzhzhkm5VvmW6Mf8EVj9SPGN4l8Lg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz", + "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==", "dev": true, "requires": { "querystringify": "^2.0.0", "requires-port": "^1.0.0" - }, - "dependencies": { - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", - "dev": true - } } }, "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "useragent": { "version": "2.2.1", @@ -11323,20 +11513,12 @@ } }, "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } + "inherits": "2.0.3" } }, "util-deprecate": { @@ -11357,9 +11539,9 @@ "dev": true }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, "uws": { @@ -11370,9 +11552,9 @@ "optional": true }, "v8flags": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.2.tgz", - "integrity": "sha512-6sgSKoFw1UpUPd3cFdF7QGnrH6tDeBgW1F3v9gy8gLY0mlbiBXq8soy8aQpY6xeeCjH5K+JvC62Acp7gtl7wWA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", + "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", "dev": true, "requires": { "homedir-polyfill": "^1.0.1" @@ -11435,9 +11617,9 @@ "dev": true }, "vscode-uri": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.3.tgz", - "integrity": "sha1-Yxvb9xbcyrDmUpGo3CXCMjIIWlI=" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.5.tgz", + "integrity": "sha1-O4majvccN/MFTXm9vdoxx7828g0=" }, "wait-on": { "version": "2.1.0", @@ -11548,23 +11730,24 @@ } }, "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.0", "braces": "^2.3.0", - "fsevents": "^1.1.2", + "fsevents": "^1.2.2", "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", "normalize-path": "^2.1.1", "path-is-absolute": "^1.0.0", "readdirp": "^2.0.0", - "upath": "^1.0.0" + "upath": "^1.0.5" } }, "debug": { @@ -11983,6 +12166,12 @@ "wordwrap": "0.0.2" } }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -12075,6 +12264,15 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -12328,23 +12526,24 @@ "dev": true }, "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.0", "braces": "^2.3.0", - "fsevents": "^1.1.2", + "fsevents": "^1.2.2", "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", "normalize-path": "^2.1.1", "path-is-absolute": "^1.0.0", "readdirp": "^2.0.0", - "upath": "^1.0.0" + "upath": "^1.0.5" } }, "expand-brackets": { @@ -12528,12 +12727,6 @@ } } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -12631,15 +12824,6 @@ "to-regex": "^3.0.2" } }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -12679,9 +12863,9 @@ } }, "webpack-merge": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.2.tgz", - "integrity": "sha512-/0QYwW/H1N/CdXYA2PNPVbsxO3u2Fpz34vs72xm03SRfg6bMNGfMJIQEpQjKRvkG2JvT6oRJFpDtSrwbX8Jzvw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz", + "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==", "dev": true, "requires": { "lodash": "^4.17.5" @@ -12736,9 +12920,9 @@ "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=" }, "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -12751,12 +12935,12 @@ "dev": true }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "window-size": { @@ -12874,11 +13058,9 @@ "dev": true }, "xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", - "dev": true, - "optional": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.2.0.tgz", + "integrity": "sha512-IyMa7SVe9FyT4WbQVW3b95mTLVceHhLEezQ02+QMvmIqDnKTxk0MLWIQPSW2MXAr1zQb+9yvwYhcyQULneh3wA==" }, "xtend": { "version": "4.0.1", diff --git a/package.json b/package.json index ec249d314d..19559f1f48 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@ngrx/store-devtools": "^5.2.0", "@ngx-translate/core": "9.1.1", "alfresco-js-api": "2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2", + "alfresco-js-api-node": "2.5.0-d5acbab9993711f37b66351a6aaedf6fc72d1ce2", "core-js": "2.5.3", "cspell": "^2.1.12", "hammerjs": "2.0.8", From 27977be9a21edd3168dc9a2719c03fb388259042 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 3 Aug 2018 14:14:19 +0300 Subject: [PATCH 067/146] [ACA-1614] DocumentList - context menu actions (#544) * context menu * make same structure check * align naming * lazy loading support * update module import implementation * close context menu on Escape * focus and navigate context menu items * update with material cdk 'keycodes' name * changed module folder name --- cspell.json | 3 +- src/app/app.module.ts | 2 + .../favorites/favorites.component.html | 5 +- src/app/components/files/files.component.html | 5 +- .../libraries/libraries.component.html | 5 +- .../recent-files/recent-files.component.html | 5 +- .../shared-files/shared-files.component.html | 5 +- .../trashcan/trashcan.component.html | 5 +- src/app/context-menu/animations.ts | 52 ++++++ .../context-menu-item.directive.ts | 51 ++++++ src/app/context-menu/context-menu-overlay.ts | 35 ++++ .../context-menu/context-menu.component.html | 12 ++ .../context-menu/context-menu.component.ts | 117 ++++++++++++ .../context-menu/context-menu.directive.ts | 66 +++++++ src/app/context-menu/context-menu.module.ts | 73 ++++++++ src/app/context-menu/context-menu.service.ts | 100 +++++++++++ src/app/context-menu/interfaces.ts | 32 ++++ src/app/extensions/extension.config.ts | 1 + src/app/extensions/extension.service.ts | 16 ++ src/assets/app.extensions.json | 169 ++++++++++++++++++ 20 files changed, 752 insertions(+), 7 deletions(-) create mode 100644 src/app/context-menu/animations.ts create mode 100644 src/app/context-menu/context-menu-item.directive.ts create mode 100644 src/app/context-menu/context-menu-overlay.ts create mode 100644 src/app/context-menu/context-menu.component.html create mode 100644 src/app/context-menu/context-menu.component.ts create mode 100644 src/app/context-menu/context-menu.directive.ts create mode 100644 src/app/context-menu/context-menu.module.ts create mode 100644 src/app/context-menu/context-menu.service.ts create mode 100644 src/app/context-menu/interfaces.ts diff --git a/cspell.json b/cspell.json index 715d8afdb5..34641d8425 100644 --- a/cspell.json +++ b/cspell.json @@ -39,7 +39,8 @@ "unindent", "exif", "cardview", - "webm" + "webm", + "keycodes" ], "dictionaries": [ "html", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5c89f9ac57..c3b5468006 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -76,6 +76,7 @@ import { DirectivesModule } from './directives/directives.module'; import { ToggleInfoDrawerComponent } from './components/toolbar/toggle-info-drawer/toggle-info-drawer.component'; import { DocumentDisplayModeComponent } from './components/toolbar/document-display-mode/document-display-mode.component'; import { ToggleFavoriteComponent } from './components/toolbar/toggle-favorite/toggle-favorite.component'; +import { ContextMenuModule } from './context-menu/context-menu.module'; export function setupExtensionServiceFactory(service: ExtensionService): Function { return () => service.load(); @@ -98,6 +99,7 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio ExtensionsModule, DirectivesModule, + ContextMenuModule.forRoot(), AppInfoDrawerModule ], declarations: [ diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 4510455078..7b029ca71e 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -13,7 +13,10 @@
- -
-
-
-
- . + */ + +import { + state, + style, + animate, + transition, + query, + group, + sequence +} from '@angular/animations'; + +export const contextMenuAnimation = [ + state('void', style({ + opacity: 0, + transform: 'scale(0.01, 0.01)' + })), + transition('void => *', sequence([ + query('.mat-menu-content', style({ opacity: 0 })), + animate('100ms linear', style({ opacity: 1, transform: 'scale(1, 0.5)' })), + group([ + query('.mat-menu-content', animate('400ms cubic-bezier(0.55, 0, 0.55, 0.2)', + style({ opacity: 1 }) + )), + animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ transform: 'scale(1, 1)' })), + ]) + ])), + transition('* => void', animate('150ms 50ms linear', style({ opacity: 0 }))) +]; diff --git a/src/app/context-menu/context-menu-item.directive.ts b/src/app/context-menu/context-menu-item.directive.ts new file mode 100644 index 0000000000..308a8423a3 --- /dev/null +++ b/src/app/context-menu/context-menu-item.directive.ts @@ -0,0 +1,51 @@ +/*! + * @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, ElementRef, OnDestroy } from '@angular/core'; +import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; + +@Directive({ + selector: '[acaContextMenuItem]', +}) +export class ContextMenuItemDirective implements OnDestroy, FocusableOption { + constructor( + private elementRef: ElementRef, + private focusMonitor: FocusMonitor) { + + focusMonitor.monitor(this.getHostElement(), false); + } + + ngOnDestroy() { + this.focusMonitor.stopMonitoring(this.getHostElement()); + } + + focus(origin: FocusOrigin = 'keyboard'): void { + this.focusMonitor.focusVia(this.getHostElement(), origin); + } + + private getHostElement(): HTMLElement { + return this.elementRef.nativeElement; + } +} diff --git a/src/app/context-menu/context-menu-overlay.ts b/src/app/context-menu/context-menu-overlay.ts new file mode 100644 index 0000000000..368138067c --- /dev/null +++ b/src/app/context-menu/context-menu-overlay.ts @@ -0,0 +1,35 @@ +/*! + * @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 { OverlayRef } from '@angular/cdk/overlay'; + +export class ContextMenuOverlayRef { + + constructor(private overlayRef: OverlayRef) { } + + close(): void { + this.overlayRef.dispose(); + } +} diff --git a/src/app/context-menu/context-menu.component.html b/src/app/context-menu/context-menu.component.html new file mode 100644 index 0000000000..dc2d449d99 --- /dev/null +++ b/src/app/context-menu/context-menu.component.html @@ -0,0 +1,12 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/src/app/context-menu/context-menu.component.ts b/src/app/context-menu/context-menu.component.ts new file mode 100644 index 0000000000..ba13249959 --- /dev/null +++ b/src/app/context-menu/context-menu.component.ts @@ -0,0 +1,117 @@ +/*! + * @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 { + Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, + ViewChildren, QueryList, AfterViewInit +} from '@angular/core'; +import { trigger } from '@angular/animations'; +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; + +import { ExtensionService } from '../extensions/extension.service'; +import { AppStore, SelectionState } from '../store/states'; +import { appSelection } from '../store/selectors/app.selectors'; +import { Store } from '@ngrx/store'; +import { Subject } from 'rxjs/Rx'; +import { takeUntil } from 'rxjs/operators'; + +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContentActionRef } from '../extensions/action.extensions'; +import { contextMenuAnimation } from './animations'; +import { ContextMenuItemDirective } from './context-menu-item.directive'; + +@Component({ + selector: 'aca-context-menu', + templateUrl: './context-menu.component.html', + host: { 'role': 'menu' }, + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('panelAnimation', contextMenuAnimation) + ] +}) +export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { + private onDestroy$: Subject = new Subject(); + private selection: SelectionState; + private _keyManager: FocusKeyManager; + actions: Array = []; + + @ViewChildren(ContextMenuItemDirective) + private contextMenuItems: QueryList; + + @HostListener('document:keydown.Escape', ['$event']) + handleKeydownEscape(event: KeyboardEvent) { + if (event) { + this.contextMenuOverlayRef.close(); + } + } + + @HostListener('document:keydown', ['$event']) + handleKeydownEvent(event: KeyboardEvent) { + if (event) { + const keyCode = event.keyCode; + if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) { + this._keyManager.onKeydown(event); + } + } + } + + constructor( + private contextMenuOverlayRef: ContextMenuOverlayRef, + private extensions: ExtensionService, + private store: Store, + ) { } + + runAction(actionId: string) { + const context = { + selection: this.selection + }; + + this.extensions.runActionById(actionId, context); + this.contextMenuOverlayRef.close(); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + ngOnInit() { + this.store + .select(appSelection) + .pipe(takeUntil(this.onDestroy$)) + .subscribe(selection => { + if (selection.count) { + this.selection = selection; + this.actions = this.extensions.getAllowedContentContextActions(); + } + }); + } + + ngAfterViewInit() { + this._keyManager = new FocusKeyManager(this.contextMenuItems); + this._keyManager.setFirstItemActive(); + } +} diff --git a/src/app/context-menu/context-menu.directive.ts b/src/app/context-menu/context-menu.directive.ts new file mode 100644 index 0000000000..0e6642860d --- /dev/null +++ b/src/app/context-menu/context-menu.directive.ts @@ -0,0 +1,66 @@ +/*! + * @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 { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContextMenuService } from './context-menu.service'; + +@Directive({ + selector: '[acaContextActions]' +}) +export class ContextActionsDirective { + private overlayRef: ContextMenuOverlayRef = null; + + // tslint:disable-next-line:no-input-rename + @Input('acaContextEnable') enabled: boolean; + + @HostListener('window:resize', ['$event']) + onResize(event) { + if (event && this.overlayRef) { + this.overlayRef.close(); + } + } + + @HostListener('contextmenu', ['$event']) + onContextmenu(event: MouseEvent) { + if (event) { + event.preventDefault(); + + if (this.enabled) { + this.render(event); + } + } + } + + constructor(private contextMenuService: ContextMenuService) { } + + private render(event: MouseEvent) { + this.overlayRef = this.contextMenuService.open({ + source: event, + hasBackdrop: true, + panelClass: 'cdk-overlay-pane', + }); + } +} diff --git a/src/app/context-menu/context-menu.module.ts b/src/app/context-menu/context-menu.module.ts new file mode 100644 index 0000000000..c670aef719 --- /dev/null +++ b/src/app/context-menu/context-menu.module.ts @@ -0,0 +1,73 @@ +/*! + * @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 { NgModule, ModuleWithProviders } from '@angular/core'; +import { MatMenuModule, MatListModule, MatIconModule, MatButtonModule } from '@angular/material'; +import { BrowserModule } from '@angular/platform-browser'; + +import { ContextActionsDirective } from './context-menu.directive'; +import { ContextMenuService } from './context-menu.service'; +import { ContextMenuComponent } from './context-menu.component'; +import { ContextMenuItemDirective } from './context-menu-item.directive'; +import { CoreModule } from '@alfresco/adf-core'; + +@NgModule({ + imports: [ + MatMenuModule, + MatListModule, + MatIconModule, + MatButtonModule, + BrowserModule, + CoreModule.forChild() + ], + declarations: [ + ContextActionsDirective, + ContextMenuComponent, + ContextMenuItemDirective + ], + exports: [ + ContextActionsDirective, + ContextMenuComponent + ], + entryComponents: [ + ContextMenuComponent + ] +}) +export class ContextMenuModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: ContextMenuModule, + providers: [ + ContextMenuService + ] + }; + } + + static forChild(): ModuleWithProviders { + return { + ngModule: ContextMenuModule + }; + } +} diff --git a/src/app/context-menu/context-menu.service.ts b/src/app/context-menu/context-menu.service.ts new file mode 100644 index 0000000000..f7bac83cec --- /dev/null +++ b/src/app/context-menu/context-menu.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Injector, ComponentRef, ElementRef } from '@angular/core'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContextMenuComponent } from './context-menu.component'; +import { ContextmenuOverlayConfig } from './interfaces'; + +@Injectable() +export class ContextMenuService { + constructor( + private injector: Injector, + private overlay: Overlay) { } + + open(config: ContextmenuOverlayConfig) { + + const overlay = this.createOverlay(config); + + const overlayRef = new ContextMenuOverlayRef(overlay); + + this.attachDialogContainer(overlay, config, overlayRef); + + overlay.backdropClick().subscribe(() => overlayRef.close()); + + // prevent native contextmenu on overlay element if config.hasBackdrop is true + (overlay)._backdropElement + .addEventListener('contextmenu', () => { + event.preventDefault(); + (overlay)._backdropClick.next(null); + }, true); + + return overlayRef; + } + + private createOverlay(config: ContextmenuOverlayConfig) { + const overlayConfig = this.getOverlayConfig(config); + return this.overlay.create(overlayConfig); + } + + private attachDialogContainer(overlay: OverlayRef, config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef) { + const injector = this.createInjector(config, contextmenuOverlayRef); + + const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector); + const containerRef: ComponentRef = overlay.attach(containerPortal); + + return containerRef.instance; + } + + private createInjector(config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef): PortalInjector { + const injectionTokens = new WeakMap(); + + injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef); + + return new PortalInjector(this.injector, injectionTokens); + } + + private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig { + const fakeElement: any = { + getBoundingClientRect: (): ClientRect => ({ + bottom: config.source.clientY, + height: 0, + left: config.source.clientX, + right: config.source.clientX, + top: config.source.clientY, + width: 0 + }) + }; + + const positionStrategy = this.overlay.position() + .connectedTo( + new ElementRef(fakeElement), + { originX: 'start', originY: 'bottom' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'start', overlayY: 'bottom' }) + .withFallbackPosition( + { originX: 'end', originY: 'top' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'end', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'end', originY: 'center' }, + { overlayX: 'start', overlayY: 'center' }) + .withFallbackPosition( + { originX: 'start', originY: 'center' }, + { overlayX: 'end', overlayY: 'center' } + ); + + const overlayConfig = new OverlayConfig({ + hasBackdrop: config.hasBackdrop, + backdropClass: config.backdropClass, + panelClass: config.panelClass, + scrollStrategy: this.overlay.scrollStrategies.close(), + positionStrategy + }); + + return overlayConfig; + } +} diff --git a/src/app/context-menu/interfaces.ts b/src/app/context-menu/interfaces.ts new file mode 100644 index 0000000000..7fb06f2508 --- /dev/null +++ b/src/app/context-menu/interfaces.ts @@ -0,0 +1,32 @@ +/*! + * @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 . + */ + +export interface ContextmenuOverlayConfig { + panelClass?: string; + hasBackdrop?: boolean; + backdropClass?: string; + source?: MouseEvent; + data?: any; +} diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index 2ba72f9e36..54b7b03996 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -46,6 +46,7 @@ export interface ExtensionConfig { navbar?: Array; content?: { actions?: Array; + contextActions?: Array }; }; } diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 3b00abf3e8..99f799e502 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -54,6 +54,7 @@ export class ExtensionService implements RuleContext { contentActions: Array = []; viewerActions: Array = []; + contentContextmenuActions: Array = []; openWithActions: Array = []; createActions: Array = []; navbar: Array = []; @@ -124,6 +125,7 @@ export class ExtensionService implements RuleContext { this.routes = this.loadRoutes(config); this.contentActions = this.loadContentActions(config); this.viewerActions = this.loadViewerActions(config); + this.contentContextmenuActions = this.loadContentContextmenuActions(config); this.openWithActions = this.loadViewerOpenWith(config); this.createActions = this.loadCreateActions(config); this.navbar = this.loadNavBar(config); @@ -173,6 +175,14 @@ export class ExtensionService implements RuleContext { return []; } + protected loadContentContextmenuActions(config: ExtensionConfig): Array { + if (config && config.features && config.features.content) { + return (config.features.content.contextActions || []) + .sort(this.sortByOrder); + } + return []; + } + protected loadNavBar(config: ExtensionConfig): any { if (config && config.features) { return (config.features.navbar || []) @@ -335,6 +345,12 @@ export class ExtensionService implements RuleContext { .filter(action => this.filterByRules(action)); } + getAllowedContentContextActions(): Array { + return this.contentContextmenuActions + .filter(this.filterEnabled) + .filter(action => this.filterByRules(action)); + } + reduceSeparators( acc: ContentActionRef[], el: ContentActionRef, diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 7e408273ab..3201761040 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -452,6 +452,175 @@ } ] } + ], + "contextActions": [ + { + "id": "app.contextmenu.download", + "type": "button", + "order": 10, + "title": "APP.ACTIONS.DOWNLOAD", + "icon": "get_app", + "actions": { + "click": "DOWNLOAD_NODES" + }, + "rules": { + "visible": "app.toolbar.canDownload" + } + }, + { + "id": "app.contextmenu.preview", + "type": "button", + "order": 15, + "title": "APP.ACTIONS.VIEW", + "icon": "open_in_browser", + "actions": { + "click": "VIEW_FILE" + }, + "rules": { + "visible": "app.toolbar.canViewFile" + } + }, + { + "id": "app.contextmenu.editFolder", + "type": "button", + "order": 20, + "title": "APP.ACTIONS.EDIT", + "icon": "create", + "actions": { + "click": "EDIT_FOLDER" + }, + "rules": { + "visible": "app.toolbar.canEditFolder" + } + }, + { + "id": "app.contextmenu.share", + "type": "button", + "title": "APP.ACTIONS.SHARE", + "order": 25, + "icon": "share", + "actions": { + "click": "SHARE_NODE" + }, + "rules": { + "visible": "app.selection.file.canShare" + } + }, + { + "id": "app.contextmenu.favorite.add", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "order": 30, + "icon": "star_border", + "actions": { + "click": "ADD_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canAdd" + } + }, + { + "id": "app.contextmenu.favorite.remove", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "order": 30, + "icon": "star", + "actions": { + "click": "REMOVE_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canRemove" + } + }, + { + "id": "app.contextmenu.copy", + "type": "button", + "title": "APP.ACTIONS.COPY", + "order": 35, + "icon": "content_copy", + "actions": { + "click": "COPY_NODES" + }, + "rules": { + "visible": "app.toolbar.canCopyNode" + } + }, + { + "id": "app.contextmenu.move", + "type": "button", + "title": "APP.ACTIONS.MOVE", + "order": 40, + "icon": "library_books", + "actions": { + "click": "MOVE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.contextmenu.delete", + "type": "button", + "title": "APP.ACTIONS.DELETE", + "order": 45, + "icon": "delete", + "actions": { + "click": "DELETE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.contextmenu.versions", + "type": "button", + "title": "APP.ACTIONS.VERSIONS", + "order": 50, + "icon": "history", + "actions": { + "click": "MANAGE_VERSIONS" + }, + "rules": { + "visible": "app.toolbar.versions" + } + }, + { + "id": "app.contextmenu.permissions", + "type": "button", + "title": "APP.ACTIONS.PERMISSIONS", + "icon": "settings_input_component", + "order": 55, + "actions": { + "click": "MANAGE_PERMISSIONS" + }, + "rules": { + "visible": "app.toolbar.permissions" + } + }, + { + "id": "app.contextmenu.purgeDeletedNodes", + "type": "button", + "title": "APP.ACTIONS.DELETE_PERMANENT", + "icon": "delete_forever", + "actions": { + "click": "PURGE_DELETED_NODES" + }, + "rules": { + "visible": "app.trashcan.hasSelection" + } + }, + { + "id": "app.contextmenu.restoreDeletedNodes", + "type": "button", + "title": "APP.ACTIONS.RESTORE", + "icon": "restore", + "actions": { + "click": "RESTORE_DELETED_NODES" + }, + "rules": { + "visible": "app.trashcan.hasSelection" + } + } ] }, "viewer": { From 744b03d22e5130ba5aedb77638128749b4ca97d2 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 3 Aug 2018 21:35:26 +0300 Subject: [PATCH 068/146] [ACA-1614] Context Menu theme (#548) * context menu theme * support for sub menus and custom components * remove dom placeholder from arrow icon * remove aboslute position * add pseudo selector --- .../context-menu/context-menu.component.html | 38 +++++++++++++++---- .../context-menu/context-menu.component.scss | 19 ++++++++++ .../context-menu.component.theme.scss | 17 +++++++++ .../context-menu/context-menu.component.ts | 9 ++++- .../context-menu/context-menu.directive.ts | 2 +- src/app/context-menu/context-menu.module.ts | 4 +- src/app/ui/custom-theme.scss | 2 + 7 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 src/app/context-menu/context-menu.component.scss create mode 100644 src/app/context-menu/context-menu.component.theme.scss diff --git a/src/app/context-menu/context-menu.component.html b/src/app/context-menu/context-menu.component.html index dc2d449d99..ca001a5abe 100644 --- a/src/app/context-menu/context-menu.component.html +++ b/src/app/context-menu/context-menu.component.html @@ -1,12 +1,34 @@
- - + + + + + + + + + + + + + + +
-
\ No newline at end of file +
diff --git a/src/app/context-menu/context-menu.component.scss b/src/app/context-menu/context-menu.component.scss new file mode 100644 index 0000000000..21289723ed --- /dev/null +++ b/src/app/context-menu/context-menu.component.scss @@ -0,0 +1,19 @@ +.aca-context-menu { + &__more-actions::after { + margin-left: 34px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 5px; + content: ''; + display: inline-block; + } + + &__separator { + display: block; + margin: 0; + padding: 0; + border-top-width: 1px; + border-top-style: solid; + } +} \ No newline at end of file diff --git a/src/app/context-menu/context-menu.component.theme.scss b/src/app/context-menu/context-menu.component.theme.scss new file mode 100644 index 0000000000..f053595a36 --- /dev/null +++ b/src/app/context-menu/context-menu.component.theme.scss @@ -0,0 +1,17 @@ +@mixin aca-context-menu-theme($theme) { + $foreground: map-get($theme, foreground); + $primary: map-get($theme, primary); + + .aca-context-menu { + @include angular-material-theme($theme); + + &__separator { + border-top-color: mat-color($foreground, divider); + } + + &__more-actions::after { + border-color: transparent; + border-left-color: mat-color($primary); + } + } +} diff --git a/src/app/context-menu/context-menu.component.ts b/src/app/context-menu/context-menu.component.ts index ba13249959..d30a56fe07 100644 --- a/src/app/context-menu/context-menu.component.ts +++ b/src/app/context-menu/context-menu.component.ts @@ -46,7 +46,14 @@ import { ContextMenuItemDirective } from './context-menu-item.directive'; @Component({ selector: 'aca-context-menu', templateUrl: './context-menu.component.html', - host: { 'role': 'menu' }, + styleUrls: [ + './context-menu.component.scss', + './context-menu.component.theme.scss' + ], + host: { + role: 'menu', + class: 'aca-context-menu' + }, encapsulation: ViewEncapsulation.None, animations: [ trigger('panelAnimation', contextMenuAnimation) diff --git a/src/app/context-menu/context-menu.directive.ts b/src/app/context-menu/context-menu.directive.ts index 0e6642860d..86683dd5d3 100644 --- a/src/app/context-menu/context-menu.directive.ts +++ b/src/app/context-menu/context-menu.directive.ts @@ -44,7 +44,7 @@ export class ContextActionsDirective { } @HostListener('contextmenu', ['$event']) - onContextmenu(event: MouseEvent) { + onContextMenuEvent(event: MouseEvent) { if (event) { event.preventDefault(); diff --git a/src/app/context-menu/context-menu.module.ts b/src/app/context-menu/context-menu.module.ts index c670aef719..bb769ce361 100644 --- a/src/app/context-menu/context-menu.module.ts +++ b/src/app/context-menu/context-menu.module.ts @@ -26,12 +26,13 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; import { MatMenuModule, MatListModule, MatIconModule, MatButtonModule } from '@angular/material'; import { BrowserModule } from '@angular/platform-browser'; +import { CoreModule } from '@alfresco/adf-core'; +import { CoreExtensionsModule } from '../extensions/core.extensions.module'; import { ContextActionsDirective } from './context-menu.directive'; import { ContextMenuService } from './context-menu.service'; import { ContextMenuComponent } from './context-menu.component'; import { ContextMenuItemDirective } from './context-menu-item.directive'; -import { CoreModule } from '@alfresco/adf-core'; @NgModule({ imports: [ @@ -40,6 +41,7 @@ import { CoreModule } from '@alfresco/adf-core'; MatIconModule, MatButtonModule, BrowserModule, + CoreExtensionsModule.forChild(), CoreModule.forChild() ], declarations: [ diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss index 9cc9a66b8a..63bd92a461 100644 --- a/src/app/ui/custom-theme.scss +++ b/src/app/ui/custom-theme.scss @@ -9,6 +9,7 @@ @import '../components/current-user/current-user.component.theme'; @import '../components/permission-manager/permissions-manager.component.theme'; @import '../dialogs/node-versions/node-versions.dialog.theme'; +@import '../context-menu/context-menu.component.theme'; @import './overrides/adf-toolbar.theme'; @import './overrides/adf-search-filter.theme'; @@ -91,4 +92,5 @@ $custom-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent); @include sidenav-component-theme($theme); @include aca-about-component-theme($theme); @include aca-current-user-theme($theme); + @include aca-context-menu-theme($theme); } From 22eac50d272d941623bc3b010e2248793adc72cb Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Sat, 4 Aug 2018 08:26:33 +0100 Subject: [PATCH 069/146] basic docs on extensibility apis (#543) basic docs on extensibility apis --- docs/extending.md | 829 ++++++++++++++++++ docs/getting-started.md | 4 + docs/help.md | 4 + docs/index.html | 5 + extension.schema.json | 6 + src/app/app.component.ts | 33 +- .../components/layout/layout.component.html | 1 + src/app/services/profile.resolver.ts | 4 +- src/app/store/actions.ts | 1 - src/app/store/actions/app.actions.ts | 35 +- src/app/store/actions/user.actions.ts | 34 - src/app/store/reducers/app.reducer.ts | 65 +- src/assets/app.extensions.json | 63 +- 13 files changed, 934 insertions(+), 150 deletions(-) create mode 100644 docs/extending.md delete mode 100644 src/app/store/actions/user.actions.ts diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 0000000000..cfc56217af --- /dev/null +++ b/docs/extending.md @@ -0,0 +1,829 @@ +--- +title: Extending +--- + +

+ Work is still in progress, the documentation and examples may change. +

+ +# Extending + +Application extensibility is performed via the root `/src/assets/app.extensions.json`, +and any number of external plugins that are references of the main entry point. + +The application also comes with the `/src/assets/plugins/` folder +already preconfigured to store external files. + +You can create plugins that change, toggle or extend the following areas: + +* Navigation sidebar links and groups +* Context Menu +* Toolbar entries + * buttons + * menu buttons + * separators +* Viewer actions + * "Open With" entries + * "More actions" toolbar entries + +Extensions can also: + +* Overwrite or disable extension points of the main application or other plugins +* Change rules, actions or any visual element +* Register new application routes based on empty pages or layouts +* Register new rule evaluators, components, guards, etc. + +## Format + +The format is represented by a JSON file with the structure similar to the following one: + +```json +{ + "$name": "app", + "$version": "1.0.0", + + "routes": [], + "actions": [], + "rules": [], + "features": {} +} +``` + +### Schema + +You can find the JSON schema at the project root folder: [extension.schema.json](https://github.com/Alfresco/alfresco-content-app/blob/master/extension.schema.json). + +

+Schema allows validating extension files, provides code completion and documentation hints. +

+ +```json +{ + "$schema": "../../extension.schema.json", + "$name": "app", + "$version": "1.0.0", +} +``` + +### Multiple files + +You can have multiple extension files distributed separately. +All additional files are linked via the `$references` property, +the order of declaration defines also the order of loading. + +```json +{ + "$schema": "../../extension.schema.json", + "$name": "app", + "$version": "1.0.0", + "$references": [ + "plugin1.json", + "plugin2.json" + ] +} +``` + +

+Always keep in mind that all extension files are merged together at runtime. +That allows plugins overwriting the code from the main application or altering other plugins. +

+ +## Routes + +To create a new route, populate the `routes` section with the corresponding entries. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "routes": [ + { + "id": "plugin1.routes.bin", + "path": "ext/bin", + "layout": "app.layout.main", + "component": "app.components.trashcan" + } + ] +} +``` + +### Route properties + +| Name | Description | +| --- | --- | +| **id** | Unique identifier. | +| **path** | Runtime path of the route. | +| **component** | The main [component](#components) to use for the route. | +| *layout* | The layout [component](#components) to use for the route. | +| *auth* | List of [authentication guards](#authentication-guards). Defaults to `[ "app.auth" ]`. | +| *data* | Custom property bag to carry with the route. | + +

+Use the `app.layout.main` value for the `layout` property to get the default application layout, +with header, navigation sidebar and main content area. +

+Leave the `layout` property empty if you want your route component take the whole page. +

+ +You can define the full route schema like in the next example: + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "routes": [ + { + "id": "plugin1.routes.bin", + "path": "ext/bin", + "component": "app.components.trashcan", + "layout": "app.layout.main", + "auth": [ "app.auth" ], + "data": { + "title": "Custom Trashcan" + } + } + ] +} +``` + +### Authentication Guards + +Below is the list of the authentication guards main application registers on startup. + +| Key | Type | Description | +| --- | --- | --- | +| app.auth | AuthGuardEcm | ADF guard, validates ACS authentication and redirects to Login if needed. | + +You can refer those guards from within your custom extensions, +or [register](#registration) your custom implementations. + +## Components + +You can register any Angular component to participate in extensibility. + +The components are used to create custom: + +* routes and pages +* toolbar buttons +* menu items + +| Key | Type | Description | +| --- | --- | --- | +| app.layout.main | LayoutComponent | Main application layout with the menu bar, navigation sidebar and main content area to project your components. | +| app.components.trashcan | TrashcanComponent | Trashcan component, used for demo purposes. | +| app.toolbar.toggleInfoDrawer | ToggleInfoDrawerComponent | The toolbar button component that toggles Info Drawer for the selection. | +| app.toolbar.toggleFavorite | ToggleFavoriteComponent | The toolbar button component that toggles Favorite state for the selection. | + +

+See [Registration](#registration) section for more details +on how to register your own entries to be re-used at runtime. +

+ +Note that custom extensions can also replace any existing component at runtime by a known identifier, +besides registering a new one. + +## Actions + +| Name | Description | +| --- | --- | +| **id** | Unique identifier. | +| **type** | Action type, see [Application Actions](#application-actions) for more details. | +| *payload* | Action payload, a string containing value or expression. | + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "actions": [ + { + "id": "plugin1.actions.settings", + "type": "NAVIGATE_URL", + "payload": "/settings" + }, + { + "id": "plugin1.actions.info", + "type": "SNACKBAR_INFO", + "payload": "I'm a nice little popup raised by extension." + }, + { + "id": "plugin1.actions.node-name", + "type": "SNACKBAR_INFO", + "payload": "$('Action for ' + context.selection.first.entry.name)" + } + ] +} +``` + +### Value expressions + +You can use light-weight expression syntax to provide custom parameters for the action payloads. + +```text +$() +``` + +Expressions are valid JavaScript blocks that evaluate to values. + +Examples: + +```text +$('hello world') // 'hello world' +$('hello' + ', ' + 'world') // 'hello, world' +$(1 + 1) // 2 +$([1, 2, 1 + 2]) // [1, 2, 3] +``` + +## Application Actions + +Application is using NgRx (Reactive libraries for Angular, inspired by Redux). +To get more information on NxRx please refer to the following resources: + +* [Comprehensive Introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510) + +Most of the application features are already exposed in the form of NgRx Actions and corresponding Effects. +You can invoke any action via a single `Store` dispatcher, similar to the following: + +```typescript +export class MyComponent { + + constructor(private store: Store) {} + + onClick() { + this.store.dispatch(new SearchByTermAction('*')); + } + +} +``` + +The code above demonstrates a simple 'click' handler that invokes `Search by Term` feature +and automatically redirects user to the **Search Results** page. + +Another example demonstrates viewing a node from a custom application service API: + +```typescript +export class MyService { + + constructor(private store: Store) {} + + viewFile(node: MinimalNodeEntity) { + this.store.dispatch(new ViewFileAction(node)); + } +} +``` + +### Using with Extensions + +You can invoke every application action from the extensions, i.e. buttons, menus, etc. + +

+Many of the actions take currently selected nodes if no payload provided. +That simplifies declaring and invoking actions from the extension files. +

+ +In the example below, we create a new entry to the "NEW" menu dropdown +and provide a new `Create Folder` command that invokes the `CREATE_FOLDER` application action. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "features": { + "create": [ + { + "id": "app.create.folder", + "type": "default", + "icon": "create_new_folder", + "title": "Create Folder", + "actions": { + "click": "CREATE_FOLDER" + } + } + ] + } +} +``` + +The `CREATE_FOLDER` action will trigger corresponding NgRx Effects to show the dialog +and perform document list reload if needed. + +Below is the list of public actions types you can use in the plugin definitions as a reference to the action: + +| Name | Payload | Description | +| --- | --- | --- | +| SET_CURRENT_FOLDER | Node | Notify components about currently opened folder. | +| SET_CURRENT_URL | string | Notify components about current browser URL. | +| SET_USER_PROFILE | Person | Assign current user profile. | +| TOGGLE_INFO_DRAWER | n/a | Toggle info drawer for the selected node. | +| ADD_FAVORITE | MinimalNodeEntity[] | Add nodes (or selection) to favorites. | +| REMOVE_FAVORITE | MinimalNodeEntity[] | Removes nodes (or selection) from favorites. | +| DELETE_LIBRARY | string | Delete a Library by id. Takes selected node if payload not provided. | +| CREATE_LIBRARY | n/a | Invoke a "Create Library" dialog. | +| SET_SELECTED_NODES | MinimalNodeEntity[] | Notify components about selected nodes. | +| DELETE_NODES | MinimalNodeEntity[] | Delete the nodes (or selection). Supports undo actions. | +| UNDO_DELETE_NODES | any[] | Reverts deletion of nodes (or selection). | +| RESTORE_DELETED_NODES | MinimalNodeEntity[] | Restores deleted nodes (or selection). Typically used with Trashcan. | +| PURGE_DELETED_NODES | MinimalNodeEntity[] | Permanently delete nodes (or selection). Typically used with Trashcan. | +| DOWNLOAD_NODES | MinimalNodeEntity[] | Download nodes (or selections). Creates a ZIP archive for folders or multiple items. | +| CREATE_FOLDER | string | Invoke a "Create Folder" dialog for the opened folder (or the parent folder id in the payload). | +| EDIT_FOLDER | MinimalNodeEntity | Invoke an "Edit Folder" dialog for the node (or selection). | +| SHARE_NODE | MinimalNodeEntity | Invoke a "Share" dialog for the node (or selection). | +| UNSHARE_NODES | MinimalNodeEntity[] | Remove nodes (or selection) from the shared nodes (does not remove content). | +| COPY_NODES | MinimalNodeEntity[] | Invoke a "Copy" dialog for the nodes (or selection). Supports undo actions. | +| MOVE_NODES | MinimalNodeEntity[] | Invoke a "Move" dialog for the nodes (or selection). Supports undo actions. | +| MANAGE_PERMISSIONS | MinimalNodeEntity | Invoke a "Manage Permissions" dialog for the node (or selection). | +| MANAGE_VERSIONS | MinimalNodeEntity | Invoke a "Manage Versions" dialog for the node (or selection). | +| NAVIGATE_URL | string | Navigate to a given route URL within the application. | +| NAVIGATE_ROUTE | any[] | Navigate to a particular Route (supports parameters) | +| NAVIGATE_FOLDER | MinimalNodeEntity | Navigate to a folder based on the Node properties. | +| NAVIGATE_PARENT_FOLDER | MinimalNodeEntity | Navigate to a containing folder based on the Node properties. | +| SEARCH_BY_TERM | string | Perform a simple search by the term and navigate to Search results. | +| SNACKBAR_INFO | string | Show information snackbar with the message provided. | +| SNACKBAR_WARNING | string | Show warning snackbar with the message provided. | +| SNACKBAR_ERROR | string | Show error snackbar with the message provided. | +| UPLOAD_FILES | n/a | Invoke "Upload Files" dialog and upload files to the currently opened folder. | +| UPLOAD_FOLDER | n/a | Invoke "Upload Folder" dialog and upload selected folder to the currently opened one. | +| VIEW_FILE | MinimalNodeEntity | Preview the file (or selection) in the Viewer. | + +## Rules + +Rules allow evaluating conditions for extension components. +For example, you can disable or hide elements based on certain rules. + +Every rule is backed by a condition evaluator. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "rules": [ + { + "id": "app.trashcan", + "type": "app.navigation.isTrashcan" + } + ] +} +``` + +Rules can accept other rules as parameters: + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "rules": [ + { + "id": "app.toolbar.favorite.canAdd", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.canAddFavorite" }, + { "type": "rule", "value": "app.navigation.isNotRecentFiles" }, + { "type": "rule", "value": "app.navigation.isNotSharedFiles" }, + { "type": "rule", "value": "app.navigation.isNotSearchResults" } + ] + } + ] +} +``` + +

+You can also negate any rule by utilizing a `!` prefix: +`!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`. +

+ +It is also possible to use inline references to registered evaluators without declaring rules, +in case you do not need providing extra parameters, or chaining multiple rules together. + +### Core Evaluators + +You can create new rules by chaining other rules and evaluators. + +| Key | Description | +| --- | --- | +| core.every | Evaluates to `true` if all chained rules evaluate to `true`. | +| core.some | Evaluates to `true` if at least one of the chained rules evaluates to `true`. | +| core.not | Evaluates to `true` if all chained rules evaluate to `false`. | + +Below is an example of the composite rule definition that combines the following conditions: + +* user has selected a single file +* user is not using **Trashcan** page + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "rules": [ + { + "id": "app.toolbar.canViewFile", + "type": "core.every", + "parameters": [ + { + "type": "rule", + "value": "app.selection.file" + }, + { + "type": "rule", + "value": "core.not", + "parameters": [ + { + "type": "rule", + "value": "app.navigation.isTrashcan" + } + ] + } + ] + } + ] +} +``` + +You can now declare a toolbar button action that is based on the rule above. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "features": { + "content": { + "actions": [ + { + "id": "app.toolbar.preview", + "type": "button", + "title": "View File", + "icon": "open_in_browser", + "actions": { + "click": "VIEW_FILE" + }, + "rules": { + "visible": "app.toolbar.canViewFile" + } + }, + ] + } + } +} +``` + +The button will be visible only when the linked rule evaluates to `true`. + +### Application Evaluators + +| Key | Description | +| --- | --- | +| app.selection.canDelete | User has permission to delete selected node(s). | +| app.selection.canDownload | User can download selected node(s). | +| app.selection.notEmpty | At least one node is selected. | +| app.selection.canUnshare | User is able to remove selected node(s) from public sharing. | +| app.selection.canAddFavorite | User can add selected node(s) to favorites. | +| app.selection.canRemoveFavorite | User can remove selected node(s) from favorites. | +| app.selection.first.canUpdate | User has permission to update selected node(s). | +| app.selection.file | A single File node is selected. | +| app.selection.file.canShare | User is able to remove selected file from public sharing. | +| app.selection.library | A single Library node is selected. | +| app.selection.folder | A single Folder node is selected. | +| app.selection.folder.canUpdate | User has permissions to update selected folder. | + +### Navigation Evaluators + +The application exposes a set of navigation-related evaluators +to help developers restrict or enable certain actions based on the route or page displayed. + +The negated evaluators are provided just to simplify development, +and to avoid having a complex rule trees just to negate the rules, +for example mixing `core.every` and `core.not`. + +

+You can also negate any rule by utilizing a `!` prefix: +`!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`. +

+ +| Key | Description | +| --- | --- | +| app.navigation.folder.canCreate | User can create content in the currently opened folder. | +| app.navigation.folder.canUpload | User can upload content to the currently opened folder. | +| app.navigation.isTrashcan | User is using **Trashcan** page. | +| app.navigation.isNotTrashcan | Current page is not a **Trashcan**. | +| app.navigation.isLibraries | User is using **Libraries** page. | +| app.navigation.isNotLibraries | Current page is not **Libraries**. | +| app.navigation.isSharedFiles | User is using **Shared Files** page. | +| app.navigation.isNotSharedFiles | Current page is not **Shared Files**. | +| app.navigation.isFavorites | User is using **Favorites** page. | +| app.navigation.isNotFavorites | Current page is not **Favorites** | +| app.navigation.isRecentFiles | User is using **Recent Files** page. | +| app.navigation.isNotRecentFiles | Current page is not **Recent Files**. | +| app.navigation.isSearchResults | User is using **Search Results** page. | +| app.navigation.isNotSearchResults | Current page is not **Search Results**. | + +

+See [Registration](#registration) section for more details +on how to register your own entries to be re-used at runtime. +

+ +#### Example + +The rule in the example below evaluates to `true` if all the conditions are met: + +- user has selected node(s) +- user is not using **Trashcan** page +- user is not using **Libraries** page + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "rules": [ + { + "id": "app.toolbar.canCopyNode", + "type": "core.every", + "parameters": [ + { "type": "rule", "value": "app.selection.notEmpty" }, + { "type": "rule", "value": "app.navigation.isNotTrashcan" }, + { "type": "rule", "value": "app.navigation.isNotLibraries" } + ] + } + ] +} +``` + +## Application Features + +### Extending Create Menu + +### Extending Navigation Sidebar + +### Extending Toolbar + +### Extending Context Menu + +### Extending Viewer + +#### Open With actions + +#### Toolbar actions + +## Registration + +You can use `ExtensionService` to register custom components, authentication guards, +rule evaluators, etc. + +It is recommended to register custom content during application startup +by utilising the `APP_INITIALIZER` injection token that comes with Angular. +In that case all plugins will be available right after main application component is ready. + +Update the main application module `app.module.ts`, or create your own module, +and use the following snippet to register custom content: + +```typescript +export function setupExtensions(extensions: ExtensionService): Function { + return () => + new Promise(resolve => { + + extensions.setComponents({ + 'plugin1.components.my': MyComponent1, + 'plugin1.layouts.my': MyLayout + }); + + extensions.setAuthGuards({ + 'plugin.auth': MyAuthGuard + }); + + extensions.setEvaluators({ + 'plugin1.rules.custom1': MyCustom1Evaluator, + 'plugin1.rules.custom2': MyCustom2Evaluator + }); + + resolve(true); + }); +} + +@NgModule({ + declarations: [ MyComponent1, MyLayout ], + entryComponents: [ MyComponent1, MyLayout ], + providers: [ + { + provide: APP_INITIALIZER, + useFactory: setupExtensions, + deps: [ ExtensionService ], + multi: true + } + ] +}) +export class MyExtensionModule {} +``` + +

+According to Angular rules, all components that are created dynamically at runtime +need to be registered within the `entryComponents` section of the NgModule. +

+ +The registration API is not limited to the custom content only. +You can replace any existing entries by replacing the values from your module. + +## Creating custom evaluator + +Rule evaluators are plain JavaScript (or TypeScript) functions +that take `RuleContext` reference and an optional list of `RuleParameter` instances. + +Application provides a special [RuleEvaluator](https://github.com/Alfresco/alfresco-content-app/blob/master/src/app/extensions/rule.extensions.ts#L30) type alias for evaluator functions: + +```typescript +export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean; +``` + +Create a function that is going to check if user has selected one or multiple nodes. + +```typescript +export function hasSelection( + context: RuleContext, + ...args: RuleParameter[] +): boolean { + return !context.selection.isEmpty; +} +``` + +The `context` is a reference to a special instance of the [RuleContext](https://github.com/Alfresco/alfresco-content-app/blob/master/src/app/extensions/rule.extensions.ts#L32) type, +that provides each evaluator access to runtime entities. + +```typescript +export interface RuleContext { + selection: SelectionState; + navigation: NavigationState; + permissions: NodePermissions; + + getEvaluator(key: string): RuleEvaluator; +} +``` + +The `SelectionState` interface exposes information about global selection state: + +```typescript +export interface SelectionState { + count: number; + nodes: MinimalNodeEntity[]; + libraries: SiteEntry[]; + isEmpty: boolean; + first?: MinimalNodeEntity; + last?: MinimalNodeEntity; + folder?: MinimalNodeEntity; + file?: MinimalNodeEntity; + library?: SiteEntry; +} +``` + +Next, register the function you have created earlier with the `ExtensionService` and give it a unique identifier: + +```typescript +extensions.setEvaluators({ + 'plugin1.rules.hasSelection': hasSelection +}); +``` + +Now, the `plugin1.rules.hasSelection` evaluator can be used as an inline rule reference, +or part of the composite rule like `core.every`. + + +

+See [Registration](#registration) section for more details +on how to register your own entries to be re-used at runtime. +

+ +## Tutorials + +### Custom route with parameters + +In this tutorial, we are going to implement the following features: + +* update the **Trashcan** component to receive and log route parameters +* create a new route that points to the **Trashcan** component and uses main layout +* create an action reference that allows redirecting to the new route +* create a button in the **New** menu that invokes an action + +Update `src/app/components/trashcan/trashcan.component.ts` and append the following code to the `ngOnInit` body: + +```typescript +import { ActivatedRoute, Params } from '@angular/router'; + +@Component({...}) +export class TrashcanComponent { + + constructor( + // ... + private route: ActivatedRoute + ) {} + + ngOnInit() { + // ... + + this.route.params.subscribe(({ nodeId }: Params) => { + console.log('node: ', nodeId); + }); + } + +} +``` + +The code above logs current route parameters to the browser console +and is a simple proof the integration works as expected. + +Next, add a new route definition as in the example below: + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "routes": [ + { + "id": "custom.routes.trashcan", + "path": "ext/trashcan/:nodeId", + "component": "app.components.trashcan", + "layout": "app.layout.main", + "auth": [ "app.auth" ] + } + ] +} +``` + +The template above creates a new route reference with the id `custom.routes.trashcan` that points to the `ext/trashcan/` route and accepts the `nodeId` parameter. + +Also, we are going to use default application layout (`app.layout.main`) +and authentication guards (`app.auth`). + +Next, create an action reference for the `NAVIGATE_ROUTE` application action +and pass route parameters: `/ext/trashcan` for the path, and `10` for the `nodeId` value. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "routes": [...], + + "actions": [ + { + "id": "custom.actions.trashcan", + "type": "NAVIGATE_ROUTE", + "payload": "$(['/ext/trashcan', '10'])" + } + ] +} +``` + +Finally, declare a new menu item for the `NEW` button and use the `custom.actions.trashcan` action created above. + +```json +{ + "$schema": "../../../extension.schema.json", + "$version": "1.0.0", + "$name": "plugin1", + + "routes": [...], + "actions": [...], + + "features": { + "create": [ + { + "id": "custom.create.trashcan", + "type": "default", + "icon": "build", + "title": "Custom trashcan route", + "actions": { + "click": "custom.actions.trashcan" + } + } + ] + } +} +``` + +Now, if you run the application, you should see a new menu item called "Custom Trashcan Route" in the "NEW" dropdown. +Upon clicking this item you should navigate to the `/ext/trashcan/10` route containing a **Trashcan** component. + +Check the browser console output and ensure you have the following output: + +```text +node: 10 +``` + +You have successfully created a new menu button that invokes your custom action +and redirects you to the extra application route. diff --git a/docs/getting-started.md b/docs/getting-started.md index 07e42cd369..7e2e771878 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,3 +1,7 @@ +--- +title: Getting Started +--- + # Getting Started ## Prerequisites diff --git a/docs/help.md b/docs/help.md index 54652ee0fc..b1cbe41765 100644 --- a/docs/help.md +++ b/docs/help.md @@ -1,3 +1,7 @@ +--- +title: Get Help +--- + # Where to get help There are several ways to get help with building applications using the Alfresco Application Development Framework: diff --git a/docs/index.html b/docs/index.html index 1ea7e81e39..2d5a42a549 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,6 +11,7 @@
+