[ACA-1508] extensions: wave 1 (#480)

* initial structure scaffold

* core extensions module

* simple navbar composition

* allow using app routes instead of registered

* migrate to new navbar setup

* remove commented out tests

* populate toolbar

* evaluate expressions

* redirect to url from toolbar

* populate "open with" viewer menu

* update test setup

* experimental flag for extensions

* test fixes

* fix tests

* code improvements, order support

* improve routing management

* populate "create" menu

* extra dictionaries for spellcheck

* allow disabling extension content

* support file/folder targets for toolbar actions

* add safety check

* navigate directly

* toolbar actions for all pages

* support route data

* "experimental" flag for "create" menu extensions

* code fixes
This commit is contained in:
Denys Vuika 2018-07-06 19:45:42 +01:00 committed by GitHub
parent 3e123bee62
commit e75042aa46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 865 additions and 141 deletions

View File

@ -40,6 +40,8 @@
"cardview" "cardview"
], ],
"dictionaries": [ "dictionaries": [
"html" "html",
"en-gb",
"en_US"
] ]
} }

View File

@ -25,7 +25,7 @@
import { browser, protractor } from 'protractor'; import { browser, protractor } from 'protractor';
import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages'; import { LoginPage, LogoutPage, BrowsingPage } from '../../pages/pages';
import { SITE_VISIBILITY, SITE_ROLES, SIDEBAR_LABELS } from '../../configs'; import { SITE_VISIBILITY, SITE_ROLES, SIDEBAR_LABELS, APP_ROUTES } from '../../configs';
import { RepoClient } from '../../utilities/repo-client/repo-client'; import { RepoClient } from '../../utilities/repo-client/repo-client';
import { Utils } from '../../utilities/utils'; import { Utils } from '../../utilities/utils';
@ -384,7 +384,8 @@ describe('Toolbar actions - multiple selection : ', () => {
}); });
beforeEach(done => { beforeEach(done => {
page.sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.SHARED_FILES) // page.sidenav.navigateToLinkByLabel(SIDEBAR_LABELS.SHARED_FILES)
browser.get(APP_ROUTES.SHARED_FILES)
.then(() => dataTable.waitForHeader()) .then(() => dataTable.waitForHeader())
.then(done); .then(done);
}); });

View File

@ -12,7 +12,8 @@
"libraries": false, "libraries": false,
"comments": false, "comments": false,
"cardview": false, "cardview": false,
"share": false "share": false,
"extensions": false
}, },
"headerColor": "#2196F3", "headerColor": "#2196F3",
"languagePicker": false, "languagePicker": false,
@ -31,65 +32,196 @@
"preserveState": true, "preserveState": true,
"expandedSidenav": true "expandedSidenav": true
}, },
"navigation": { "extensions": {
"main": [ "external": [
{ "plugin1.json",
"icon": "folder", "plugin2.json"
"label": "APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL",
"title": "APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP",
"disabled": false,
"route": {
"url": "/personal-files"
}
},
{
"icon": "group_work",
"label": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL",
"title": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP",
"disabled": false,
"route": {
"url": "/libraries"
}
}
], ],
"secondary": [ "core": {
{ "routes": [
"icon": "people", {
"label": "APP.BROWSE.SHARED.SIDENAV_LINK.LABEL", "id": "aca:routes/about",
"title": "APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP", "path": "ext/about",
"disabled": false, "component": "aca:components/about",
"route": { "layout": "aca:layouts/main",
"url": "/shared" "auth":[ "aca:auth" ],
"data": {
"title": "Custom About"
}
} }
}, ],
{ "actions": [
"icon": "schedule", {
"label": "APP.BROWSE.RECENT.SIDENAV_LINK.LABEL", "id": "aca:actions/info",
"title": "APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP", "type": "SNACKBAR_INFO",
"disabled": false, "payload": "I'm a nice little popup raised by extension."
"route": { },
"url": "/recent-files" {
"id": "aca:actions/error",
"type": "SNACKBAR_ERROR",
"payload": "Aw, Snap!"
},
{
"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": {
"icon": "star", "create": [
"label": "APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL", {
"title": "APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP", "id": "aca:create/action1",
"disabled": false, "order": 100,
"route": { "icon": "build",
"url": "/favorites" "title": "Error",
} "action": "aca:actions/error"
}, }
{ ],
"icon": "delete", "navigation": {
"label": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL", "aca:main": [
"title": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP", {
"disabled": false, "id": "aca/personal-files",
"route": { "order": 100,
"url": "/trashcan" "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": [
{
"disabled": false,
"id": "aca:action1",
"order": 100,
"title": "Info",
"icon": "build",
"target": {
"type": "folder",
"permissions": ["one", "two"],
"action": "aca:actions/info"
}
},
{
"disabled": false,
"id": "aca:action2",
"order": 101,
"title": "Node name",
"icon": "feedback",
"target": {
"type": "folder",
"permissions": ["one", "two"],
"action": "aca:actions/node-name"
}
},
{
"disabled": false,
"id": "aca:action3",
"order": 101,
"title": "Settings",
"icon": "settings_applications",
"target": {
"type": "folder",
"permissions": ["one", "two"],
"action": "aca:actions/settings"
}
},
{
"disabled": false,
"id": "aca:action4",
"order": 101,
"title": "Error",
"icon": "report_problem",
"target": {
"type": "file",
"permissions": ["one", "two"],
"action": "aca:actions/error"
}
}
]
} }
} }
] }
}, },
"languages": [ "languages": [
{ {

View File

@ -37,6 +37,7 @@ import {
SetLanguagePickerAction, SetLanguagePickerAction,
SetSharedUrlAction SetSharedUrlAction
} from './store/actions'; } from './store/actions';
import { ExtensionService } from './extensions/extension.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -51,7 +52,8 @@ export class AppComponent implements OnInit {
private store: Store<AppStore>, private store: Store<AppStore>,
private config: AppConfigService, private config: AppConfigService,
private alfrescoApiService: AlfrescoApiService, private alfrescoApiService: AlfrescoApiService,
private authenticationService: AuthenticationService) { private authenticationService: AuthenticationService,
private extensions: ExtensionService) {
} }
ngOnInit() { ngOnInit() {
@ -83,6 +85,12 @@ export class AppComponent implements OnInit {
pageTitle.setTitle(data.title || ''); pageTitle.setTitle(data.title || '');
}); });
this.extensions.init();
this.router.config.unshift(
...this.extensions.getApplicationRoutes()
);
} }
private loadAppSettings() { private loadAppSettings() {

View File

@ -82,6 +82,9 @@ import { DocumentListDirective } from './directives/document-list.directive';
import { MaterialModule } from './material.module'; import { MaterialModule } from './material.module';
import { ExperimentalDirective } from './directives/experimental.directive'; import { ExperimentalDirective } from './directives/experimental.directive';
import { ContentApiService } from './services/content-api.service'; import { ContentApiService } from './services/content-api.service';
import { ExtensionsModule } from './extensions.module';
import { ExtensionService } from './extensions/extension.service';
import { CoreExtensionsModule } from './extensions/core.extensions';
@NgModule({ @NgModule({
imports: [ imports: [
@ -96,7 +99,9 @@ import { ContentApiService } from './services/content-api.service';
MaterialModule, MaterialModule,
CoreModule, CoreModule,
ContentModule, ContentModule,
AppStoreModule AppStoreModule,
CoreExtensionsModule,
ExtensionsModule
], ],
declarations: [ declarations: [
AppComponent, AppComponent,
@ -155,7 +160,8 @@ import { ContentApiService } from './services/content-api.service';
NodePermissionService, NodePermissionService,
ProfileResolver, ProfileResolver,
ExperimentalGuard, ExperimentalGuard,
ContentApiService ContentApiService,
ExtensionService
], ],
entryComponents: [ entryComponents: [
NodeVersionsDialogComponent NodeVersionsDialogComponent

View File

@ -10,7 +10,15 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
mat-icon-button mat-icon-button
color="primary" color="primary"

View File

@ -37,6 +37,7 @@ import { NodePermissionService } from '../../common/services/node-permission.ser
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './favorites.component.html' templateUrl: './favorites.component.html'
@ -45,11 +46,12 @@ export class FavoritesComponent extends PageComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
store: Store<AppStore>, store: Store<AppStore>,
extensions: ExtensionService,
private contentApi: ContentApiService, private contentApi: ContentApiService,
private content: ContentManagementService, private content: ContentManagementService,
public permission: NodePermissionService public permission: NodePermissionService
) { ) {
super(store); super(store, extensions);
} }
ngOnInit() { ngOnInit() {

View File

@ -13,6 +13,15 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
color="primary" color="primary"
mat-icon-button mat-icon-button

View File

@ -36,6 +36,7 @@ import { NodePermissionService } from '../../common/services/node-permission.ser
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './files.component.html' templateUrl: './files.component.html'
@ -54,8 +55,9 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
private uploadService: UploadService, private uploadService: UploadService,
private contentManagementService: ContentManagementService, private contentManagementService: ContentManagementService,
private browsingFilesService: BrowsingFilesService, private browsingFilesService: BrowsingFilesService,
public permission: NodePermissionService) { public permission: NodePermissionService,
super(store); extensions: ExtensionService) {
super(store, extensions);
} }
ngOnInit() { ngOnInit() {

View File

@ -34,9 +34,9 @@ import {
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ShareDataTableAdapter } from '@alfresco/adf-content-services'; import { ShareDataTableAdapter } from '@alfresco/adf-content-services';
import { LibrariesComponent } from './libraries.component'; import { LibrariesComponent } from './libraries.component';
import { ExperimentalDirective } from '../../directives/experimental.directive';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('LibrariesComponent', () => { describe('LibrariesComponent', () => {
let fixture: ComponentFixture<LibrariesComponent>; let fixture: ComponentFixture<LibrariesComponent>;

View File

@ -34,6 +34,7 @@ import { DeleteLibraryAction } from '../../store/actions';
import { SiteEntry } from 'alfresco-js-api'; import { SiteEntry } from 'alfresco-js-api';
import { ContentManagementService } from '../../common/services/content-management.service'; import { ContentManagementService } from '../../common/services/content-management.service';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './libraries.component.html' templateUrl: './libraries.component.html'
@ -44,8 +45,9 @@ export class LibrariesComponent extends PageComponent implements OnInit {
private content: ContentManagementService, private content: ContentManagementService,
private contentApi: ContentApiService, private contentApi: ContentApiService,
store: Store<AppStore>, store: Store<AppStore>,
extensions: ExtensionService,
private router: Router) { private router: Router) {
super(store); super(store, extensions);
} }
ngOnInit() { ngOnInit() {

View File

@ -29,7 +29,7 @@ class TestClass extends PageComponent {
node: any; node: any;
constructor() { constructor() {
super(null); super(null, null);
} }
} }

View File

@ -35,6 +35,8 @@ import { appSelection, sharedUrl } from '../store/selectors/app.selectors';
import { AppStore } from '../store/states/app.state'; import { AppStore } from '../store/states/app.state';
import { SelectionState } from '../store/states/selection.state'; import { SelectionState } from '../store/states/selection.state';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { ExtensionService } from '../extensions/extension.service';
import { ContentActionExtension } from '../extensions/content-action.extension';
export abstract class PageComponent implements OnInit, OnDestroy { export abstract class PageComponent implements OnInit, OnDestroy {
@ -49,6 +51,7 @@ export abstract class PageComponent implements OnInit, OnDestroy {
selection: SelectionState; selection: SelectionState;
displayMode = DisplayMode.List; displayMode = DisplayMode.List;
sharedPreviewUrl$: Observable<string>; sharedPreviewUrl$: Observable<string>;
actions: Array<ContentActionExtension> = [];
protected subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
@ -56,7 +59,9 @@ export abstract class PageComponent implements OnInit, OnDestroy {
return node.isLocked || (node.properties && node.properties['cm:lockType'] === 'READ_ONLY_LOCK'); return node.isLocked || (node.properties && node.properties['cm:lockType'] === 'READ_ONLY_LOCK');
} }
constructor(protected store: Store<AppStore>) {} constructor(
protected store: Store<AppStore>,
protected extensions: ExtensionService) {}
ngOnInit() { ngOnInit() {
this.sharedPreviewUrl$ = this.store.select(sharedUrl); this.sharedPreviewUrl$ = this.store.select(sharedUrl);
@ -68,6 +73,21 @@ export abstract class PageComponent implements OnInit, OnDestroy {
this.selection = selection; this.selection = selection;
if (selection.isEmpty) { if (selection.isEmpty) {
this.infoDrawerOpened = false; 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;
});
} }
}); });
} }
@ -144,4 +164,14 @@ export abstract class PageComponent implements OnInit, OnDestroy {
this.displayMode = this.displayMode === DisplayMode.List ? DisplayMode.Gallery : DisplayMode.List; this.displayMode = this.displayMode === DisplayMode.List ? DisplayMode.Gallery : DisplayMode.List;
this.documentList.display = this.displayMode; this.documentList.display = this.displayMode;
} }
// 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);
}
} }

View File

@ -35,6 +35,15 @@
(navigateBefore)="onNavigateBefore()" (navigateBefore)="onNavigateBefore()"
(navigateNext)="onNavigateNext()"> (navigateNext)="onNavigateNext()">
<adf-viewer-open-with *ifExperimental="'extensions'">
<button *ngFor="let entry of openWith"
mat-menu-item
(click)="runAction(entry.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
<span>{{ entry.title }}</span>
</button>
</adf-viewer-open-with>
<adf-viewer-more-actions> <adf-viewer-more-actions>
<button <button

View File

@ -33,6 +33,8 @@ import { AppStore } from '../../store/states/app.state';
import { DeleteNodesAction } from '../../store/actions'; import { DeleteNodesAction } from '../../store/actions';
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { ExtensionService } from '../../extensions/extension.service';
import { OpenWithExtension } from '../../extensions/open-with.extension';
@Component({ @Component({
selector: 'app-preview', selector: 'app-preview',
templateUrl: 'preview.component.html', templateUrl: 'preview.component.html',
@ -55,6 +57,7 @@ export class PreviewComponent extends PageComponent implements OnInit {
navigateMultiple = false; navigateMultiple = false;
selectedEntities: MinimalNodeEntity[] = []; selectedEntities: MinimalNodeEntity[] = [];
openWith: Array<OpenWithExtension> = [];
constructor( constructor(
private contentApi: ContentApiService, private contentApi: ContentApiService,
@ -63,9 +66,9 @@ export class PreviewComponent extends PageComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
store: Store<AppStore>, store: Store<AppStore>,
public permission: NodePermissionService) { public permission: NodePermissionService,
extensions: ExtensionService) {
super(store); super(store, extensions);
} }
ngOnInit() { ngOnInit() {
@ -97,6 +100,8 @@ export class PreviewComponent extends PageComponent implements OnInit {
this.subscriptions = this.subscriptions.concat([ this.subscriptions = this.subscriptions.concat([
this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error)) this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error))
]); ]);
this.openWith = this.extensions.openWithActions;
} }
/** /**
@ -359,4 +364,10 @@ export class PreviewComponent extends PageComponent implements OnInit {
return acc; 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);
}
} }

View File

@ -10,7 +10,15 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
mat-icon-button mat-icon-button
color="primary" color="primary"

View File

@ -32,6 +32,7 @@ import { PageComponent } from '../page.component';
import { NodePermissionService } from '../../common/services/node-permission.service'; import { NodePermissionService } from '../../common/services/node-permission.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './recent-files.component.html' templateUrl: './recent-files.component.html'
@ -40,10 +41,11 @@ export class RecentFilesComponent extends PageComponent implements OnInit {
constructor( constructor(
store: Store<AppStore>, store: Store<AppStore>,
extensions: ExtensionService,
private uploadService: UploadService, private uploadService: UploadService,
private content: ContentManagementService, private content: ContentManagementService,
public permission: NodePermissionService) { public permission: NodePermissionService) {
super(store); super(store, extensions);
} }
ngOnInit() { ngOnInit() {

View File

@ -3,6 +3,15 @@
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE"> <adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE">
</adf-breadcrumb> </adf-breadcrumb>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
color="primary" color="primary"
mat-icon-button mat-icon-button

View File

@ -31,6 +31,7 @@ import { PageComponent } from '../page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { NavigateToFolder } from '../../store/actions'; import { NavigateToFolder } from '../../store/actions';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
selector: 'app-search', selector: 'app-search',
@ -53,9 +54,10 @@ export class SearchComponent extends PageComponent implements OnInit {
public permission: NodePermissionService, public permission: NodePermissionService,
private queryBuilder: SearchQueryBuilderService, private queryBuilder: SearchQueryBuilderService,
private route: ActivatedRoute, private route: ActivatedRoute,
store: Store<AppStore> store: Store<AppStore>,
extensions: ExtensionService
) { ) {
super(store); super(store, extensions);
queryBuilder.paging = { queryBuilder.paging = {
skipCount: 0, skipCount: 0,

View File

@ -80,7 +80,6 @@
Cardview Cardview
</mat-checkbox> </mat-checkbox>
</div> </div>
<div> <div>
<mat-checkbox <mat-checkbox
[(ngModel)]="share" [(ngModel)]="share"
@ -88,5 +87,12 @@
Share Share
</mat-checkbox> </mat-checkbox>
</div> </div>
<div>
<mat-checkbox
[(ngModel)]="extensions"
(change)="onChangeExtensionsFeature($event)">
Extensions
</mat-checkbox>
</div>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>

View File

@ -52,6 +52,7 @@ export class SettingsComponent implements OnInit {
comments: boolean; comments: boolean;
cardview: boolean; cardview: boolean;
share: boolean; share: boolean;
extensions: boolean;
constructor( constructor(
private store: Store<AppStore>, private store: Store<AppStore>,
@ -86,6 +87,9 @@ export class SettingsComponent implements OnInit {
const share = this.appConfig.get('experimental.share'); const share = this.appConfig.get('experimental.share');
this.share = (share === true || share === 'true'); this.share = (share === true || share === 'true');
const extensions = this.appConfig.get('experimental.extensions');
this.extensions = (extensions === true || extensions === 'true');
} }
apply(model: any, isValid: boolean) { apply(model: any, isValid: boolean) {
@ -121,4 +125,8 @@ export class SettingsComponent implements OnInit {
onChangeShareFeature(event: MatCheckboxChange) { onChangeShareFeature(event: MatCheckboxChange) {
this.storage.setItem('experimental.share', event.checked.toString()); this.storage.setItem('experimental.share', event.checked.toString());
} }
onChangeExtensionsFeature(event: MatCheckboxChange) {
this.storage.setItem('experimental.extensions', event.checked.toString());
}
} }

View File

@ -10,6 +10,15 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
*ngIf="selection.count === 1" *ngIf="selection.count === 1"
color="primary" color="primary"

View File

@ -31,17 +31,20 @@ import { NodePermissionService } from '../../common/services/node-permission.ser
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './shared-files.component.html' templateUrl: './shared-files.component.html'
}) })
export class SharedFilesComponent extends PageComponent implements OnInit { export class SharedFilesComponent extends PageComponent implements OnInit {
constructor(
constructor(store: Store<AppStore>, store: Store<AppStore>,
private uploadService: UploadService, extensions: ExtensionService,
private content: ContentManagementService, private uploadService: UploadService,
public permission: NodePermissionService) { private content: ContentManagementService,
super(store); public permission: NodePermissionService
) {
super(store, extensions);
} }
ngOnInit() { ngOnInit() {
@ -52,7 +55,9 @@ export class SharedFilesComponent extends PageComponent implements OnInit {
this.content.nodesMoved.subscribe(() => this.reload()), this.content.nodesMoved.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()),
this.content.linksUnshared.subscribe(() => this.reload()), this.content.linksUnshared.subscribe(() => this.reload()),
this.uploadService.fileUploadError.subscribe((error) => this.onFileUploadedError(error)) this.uploadService.fileUploadError.subscribe(error =>
this.onFileUploadedError(error)
)
]); ]);
} }
} }

View File

@ -6,6 +6,14 @@
<mat-icon [title]="'APP.NEW_MENU.TOOLTIP' | translate">queue</mat-icon> <mat-icon [title]="'APP.NEW_MENU.TOOLTIP' | translate">queue</mat-icon>
</div> </div>
<div sidebar-menu-options> <div sidebar-menu-options>
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of createActions"
mat-menu-item
(click)="runAction(entry.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
<span>{{ entry.title | translate }}</span>
</button>
</ng-container>
<button <button
mat-menu-item mat-menu-item
[disabled]="!permission.check(node, ['create'])" [disabled]="!permission.check(node, ['create'])"
@ -48,16 +56,17 @@
</adf-sidebar-action-menu> </adf-sidebar-action-menu>
</div> </div>
<div class="sidenav__section sidenav__section--menu" *ngFor="let list of navigation"> <div class="sidenav__section sidenav__section--menu" *ngFor="let group of groups">
<ul class="sidenav-menu"> <ul class="sidenav-menu">
<li *ngFor="let item of list" class="sidenav-menu__item" <li *ngFor="let item of group" class="sidenav-menu__item"
routerLinkActive routerLinkActive
#rla="routerLinkActive" #rla="routerLinkActive"
title="{{ item.title || '' | translate }}"> title="{{ item.description | translate }}">
<button [routerLink]="item.route.url" <button
[routerLink]="item.route"
[color]="rla.isActive ? 'accent': 'primary'" [color]="rla.isActive ? 'accent': 'primary'"
[attr.aria-label]="item.label | translate" [attr.aria-label]="item.title | translate"
mat-icon-button mat-icon-button
mat-ripple mat-ripple
matRippleColor="primary" matRippleColor="primary"
@ -68,13 +77,13 @@
</button> </button>
<span #rippleTrigger <span #rippleTrigger
[routerLink]="item.route"
class="menu__item--label" class="menu__item--label"
[routerLink]="item.route.url"
[hidden]="!showLabel" [hidden]="!showLabel"
[ngClass]="{ [ngClass]="{
'menu__item--active': rla.isActive, 'menu__item--active': rla.isActive,
'menu__item--default': !rla.isActive 'menu__item--default': !rla.isActive
}">{{ item.label | translate }}</span> }">{{ item.title | translate }}</span>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -25,26 +25,17 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { AppConfigService } from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service'; import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { SidenavComponent } from './sidenav.component'; import { SidenavComponent } from './sidenav.component';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { NodeEffects } from '../../store/effects/node.effects'; import { NodeEffects } from '../../store/effects/node.effects';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('SidenavComponent', () => { describe('SidenavComponent', () => {
let fixture: ComponentFixture<SidenavComponent>; let fixture: ComponentFixture<SidenavComponent>;
let component: SidenavComponent; let component: SidenavComponent;
let browsingService: BrowsingFilesService; let browsingService: BrowsingFilesService;
let appConfig: AppConfigService;
let appConfigSpy;
const navItem = {
label: 'some-label',
route: {
url: '/some-url'
}
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -53,19 +44,17 @@ describe('SidenavComponent', () => {
EffectsModule.forRoot([NodeEffects]) EffectsModule.forRoot([NodeEffects])
], ],
declarations: [ declarations: [
SidenavComponent SidenavComponent,
ExperimentalDirective
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]
}) })
.compileComponents() .compileComponents()
.then(() => { .then(() => {
browsingService = TestBed.get(BrowsingFilesService); browsingService = TestBed.get(BrowsingFilesService);
appConfig = TestBed.get(AppConfigService);
fixture = TestBed.createComponent(SidenavComponent); fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
appConfigSpy = spyOn(appConfig, 'get').and.returnValue([navItem]);
}); });
})); }));
@ -77,20 +66,4 @@ describe('SidenavComponent', () => {
expect(component.node).toBe(node); expect(component.node).toBe(node);
}); });
describe('menu', () => {
it('should build menu from array', () => {
appConfigSpy.and.returnValue([navItem, navItem]);
fixture.detectChanges();
expect(component.navigation).toEqual([[navItem, navItem]]);
});
it('should build menu from object', () => {
appConfigSpy.and.returnValue({ a: [navItem, navItem], b: [navItem, navItem] });
fixture.detectChanges();
expect(component.navigation).toEqual([[navItem, navItem], [navItem, navItem]]);
});
});
}); });

View File

@ -26,11 +26,11 @@
import { Subscription } from 'rxjs/Rx'; import { Subscription } from 'rxjs/Rx';
import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AppConfigService } from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service'; import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service'; import { NodePermissionService } from '../../common/services/node-permission.service';
import { ExtensionService } from '../../extensions/extension.service';
import { NavigationExtension } from '../../extensions/navigation.extension';
import { CreateExtension } from '../../extensions/create.extension';
@Component({ @Component({
selector: 'app-sidenav', selector: 'app-sidenav',
@ -41,22 +41,25 @@ export class SidenavComponent implements OnInit, OnDestroy {
@Input() showLabel: boolean; @Input() showLabel: boolean;
node: MinimalNodeEntryEntity = null; node: MinimalNodeEntryEntity = null;
navigation = []; groups: Array<NavigationExtension[]> = [];
createActions: Array<CreateExtension> = [];
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor( constructor(
private browsingFilesService: BrowsingFilesService, private browsingFilesService: BrowsingFilesService,
private appConfig: AppConfigService, public permission: NodePermissionService,
public permission: NodePermissionService private extensions: ExtensionService
) {} ) {}
ngOnInit() { ngOnInit() {
this.navigation = this.buildMenu(); this.groups = this.extensions.getNavigationGroups();
this.createActions = this.extensions.createActions;
this.subscriptions.concat([ this.subscriptions.concat([
this.browsingFilesService.onChangeParent this.browsingFilesService.onChangeParent.subscribe(
.subscribe((node: MinimalNodeEntryEntity) => this.node = node) (node: MinimalNodeEntryEntity) => (this.node = node)
)
]); ]);
} }
@ -64,10 +67,9 @@ export class SidenavComponent implements OnInit, OnDestroy {
this.subscriptions.forEach(s => s.unsubscribe()); this.subscriptions.forEach(s => s.unsubscribe());
} }
private buildMenu() { // this is where each application decides how to treat an action and what to do
const schema = this.appConfig.get('navigation'); // the ACA maps actions to the NgRx actions as an example
const data = Array.isArray(schema) ? { main: schema } : schema; runAction(actionId: string) {
this.extensions.runActionById(actionId);
return Object.keys(data).map((key) => data[key]);
} }
} }

View File

@ -10,6 +10,15 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<adf-toolbar class="inline" *ngIf="!selection.isEmpty"> <adf-toolbar class="inline" *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'extensions'">
<button *ngFor="let entry of actions"
mat-icon-button
color="primary"
title="{{ entry.title | translate }}"
(click)="runAction(entry.target.action)">
<mat-icon>{{ entry.icon }}</mat-icon>
</button>
</ng-container>
<button <button
color="primary" color="primary"
mat-icon-button mat-icon-button

View File

@ -30,6 +30,7 @@ import { Store } from '@ngrx/store';
import { selectUser } from '../../store/selectors/app.selectors'; import { selectUser } from '../../store/selectors/app.selectors';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { ProfileState } from '../../store/states/profile.state'; import { ProfileState } from '../../store/states/profile.state';
import { ExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './trashcan.component.html' templateUrl: './trashcan.component.html'
@ -38,8 +39,9 @@ export class TrashcanComponent extends PageComponent implements OnInit {
user: ProfileState; user: ProfileState;
constructor(private contentManagementService: ContentManagementService, constructor(private contentManagementService: ContentManagementService,
extensions: ExtensionService,
store: Store<AppStore>) { store: Store<AppStore>) {
super(store); super(store, extensions);
} }
ngOnInit() { ngOnInit() {

View File

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
// Main entry point for external extensions only.
// For any application-specific code use CoreExtensionsModule instead.
@NgModule({
imports: []
})
export class ExtensionsModule {}

View File

@ -0,0 +1,30 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
export interface ActionExtension {
id: string;
type: string;
payload?: string;
}

View File

@ -0,0 +1,37 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
export interface ContentActionExtension {
id: string;
order?: number;
title: string;
icon?: string;
disabled?: boolean;
target: {
type: string;
permissions: Array<string>,
action: string;
};
}

View File

@ -0,0 +1,43 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { AuthGuardEcm } from '@alfresco/adf-core';
import { ExtensionService } from './extension.service';
import { AboutComponent } from '../components/about/about.component';
import { LayoutComponent } from '../components/layout/layout.component';
@NgModule({
imports: [],
declarations: [],
entryComponents: [AboutComponent]
})
export class CoreExtensionsModule {
constructor(extensions: ExtensionService) {
extensions.components['aca:layouts/main'] = LayoutComponent;
extensions.components['aca:components/about'] = AboutComponent;
extensions.authGuards['aca:auth'] = AuthGuardEcm;
}
}

View File

@ -0,0 +1,33 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
export interface CreateExtension {
id: string;
order?: number;
title: string;
icon?: string;
action: string;
disabled?: boolean;
}

View File

@ -0,0 +1,186 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
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 { 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 { CreateExtension } from './create.extension';
@Injectable()
export class ExtensionService {
routes: Array<RouteExtension> = [];
actions: Array<ActionExtension> = [];
contentActions: Array<ContentActionExtension> = [];
openWithActions: Array<OpenWithExtension> = [];
createActions: Array<CreateExtension> = [];
authGuards: { [key: string]: Type<{}> } = {};
components: { [key: string]: Type<{}> } = {};
constructor(
private config: AppConfigService,
private store: Store<AppStore>
) {}
// initialise extension service
// in future will also load and merge data from the external plugins
init() {
this.routes = this.config.get<Array<RouteExtension>>(
'extensions.core.routes',
[]
);
this.actions = this.config.get<Array<ActionExtension>>(
'extensions.core.actions',
[]
);
this.contentActions = this.config
.get<Array<ContentActionExtension>>(
'extensions.core.features.content.actions',
[]
)
.filter(entry => !entry.disabled)
.sort(this.sortByOrder);
this.openWithActions = this.config
.get<Array<OpenWithExtension>>(
'extensions.core.features.viewer.open-with',
[]
)
.filter(entry => !entry.disabled)
.sort(this.sortByOrder);
this.createActions = this.config
.get<Array<CreateExtension>>('extensions.core.features.create', [])
.filter(entry => !entry.disabled)
.sort(this.sortByOrder);
}
getRouteById(id: string): RouteExtension {
return this.routes.find(route => route.id === id);
}
getActionById(id: string): ActionExtension {
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 });
}
}
getNavigationGroups(): Array<NavigationExtension[]> {
const settings = this.config.get<any>(
'extensions.core.features.navigation'
);
if (settings) {
const groups = Object.keys(settings).map(key => {
return settings[key]
.map(group => {
const customRoute = this.getRouteById(group.route);
const route = `/${
customRoute ? customRoute.path : group.route
}`;
return {
...group,
route
};
})
.filter(entry => !entry.disabled);
});
return groups;
}
return [];
}
getAuthGuards(ids: string[] = []): Array<Type<{}>> {
return ids.map(id => this.authGuards[id]);
}
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<Route> {
return this.routes.map(route => {
const guards = this.getAuthGuards(route.auth);
return {
path: route.path,
component: this.getComponentById(route.layout),
canActivateChild: guards,
canActivate: guards,
children: [
{
path: '',
component: this.getComponentById(route.component),
data: route.data
}
],
};
});
}
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
export interface NavigationExtension {
id: string;
order: number;
icon: string;
title: string;
route: string;
description?: string;
disabled?: boolean;
}

View File

@ -0,0 +1,33 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
export interface OpenWithExtension {
id: string;
order?: number;
icon: string;
title: string;
action: string;
disabled?: boolean;
}

View File

@ -0,0 +1,33 @@
/*!
* @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 <http://www.gnu.org/licenses/>.
*/
export interface RouteExtension {
id: string;
path: string;
component: string;
layout: string;
auth: string[];
data?: { [key: string]: string };
}

View File

@ -26,10 +26,16 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
export const NAVIGATE_URL = 'NAVIGATE_URL';
export const NAVIGATE_ROUTE = 'NAVIGATE_ROUTE'; export const NAVIGATE_ROUTE = 'NAVIGATE_ROUTE';
export const NAVIGATE_FOLDER = 'NAVIGATE_FOLDER'; export const NAVIGATE_FOLDER = 'NAVIGATE_FOLDER';
export const NAVIGATE_PARENT_FOLDER = 'NAVIGATE_PARENT_FOLDER'; export const NAVIGATE_PARENT_FOLDER = 'NAVIGATE_PARENT_FOLDER';
export class NavigateUrlAction implements Action {
readonly type = NAVIGATE_URL;
constructor(public payload: string) {}
}
export class NavigateRouteAction implements Action { export class NavigateRouteAction implements Action {
readonly type = NAVIGATE_ROUTE; readonly type = NAVIGATE_ROUTE;
constructor(public payload: any[]) {} constructor(public payload: any[]) {}
@ -40,7 +46,6 @@ export class NavigateToFolder implements Action {
constructor(public payload: MinimalNodeEntity) {} constructor(public payload: MinimalNodeEntity) {}
} }
export class NavigateToParentFolder implements Action { export class NavigateToParentFolder implements Action {
readonly type = NAVIGATE_PARENT_FOLDER; readonly type = NAVIGATE_PARENT_FOLDER;
constructor(public payload: MinimalNodeEntity) {} constructor(public payload: MinimalNodeEntity) {}

View File

@ -34,12 +34,22 @@ import {
NAVIGATE_PARENT_FOLDER, NAVIGATE_PARENT_FOLDER,
NAVIGATE_ROUTE NAVIGATE_ROUTE
} from '../actions'; } from '../actions';
import { NavigateToFolder, NAVIGATE_FOLDER } from '../actions/router.actions'; import { NavigateToFolder, NAVIGATE_FOLDER, NavigateUrlAction, NAVIGATE_URL } from '../actions/router.actions';
@Injectable() @Injectable()
export class RouterEffects { export class RouterEffects {
constructor(private actions$: Actions, private router: Router) {} constructor(private actions$: Actions, private router: Router) {}
@Effect({ dispatch: false })
navigateUrl$ = this.actions$.pipe(
ofType<NavigateUrlAction>(NAVIGATE_URL),
map(action => {
if (action.payload) {
this.router.navigateByUrl(action.payload);
}
})
);
@Effect({ dispatch: false }) @Effect({ dispatch: false })
navigateRoute$ = this.actions$.pipe( navigateRoute$ = this.actions$.pipe(
ofType<NavigateRouteAction>(NAVIGATE_ROUTE), ofType<NavigateRouteAction>(NAVIGATE_ROUTE),

View File

@ -82,7 +82,7 @@ export class SnackbarEffects {
} }
const snackBarRef = this.snackBar.open(message, actionName, { const snackBarRef = this.snackBar.open(message, actionName, {
duration: action.duration, duration: action.duration || 4000,
panelClass: panelClass panelClass: panelClass
}); });

View File

@ -62,6 +62,7 @@ import { NodeActionsService } from '../common/services/node-actions.service';
import { NodePermissionService } from '../common/services/node-permission.service'; import { NodePermissionService } from '../common/services/node-permission.service';
import { BrowsingFilesService } from '../common/services/browsing-files.service'; import { BrowsingFilesService } from '../common/services/browsing-files.service';
import { ContentApiService } from '../services/content-api.service'; import { ContentApiService } from '../services/content-api.service';
import { ExtensionService } from '../extensions/extension.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -76,7 +77,11 @@ import { ContentApiService } from '../services/content-api.service';
EffectsModule.forRoot([]) EffectsModule.forRoot([])
], ],
declarations: [TranslatePipeMock], declarations: [TranslatePipeMock],
exports: [TranslatePipeMock, RouterTestingModule, MaterialModule], exports: [
TranslatePipeMock,
RouterTestingModule,
MaterialModule,
],
providers: [ providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiMock }, { provide: AlfrescoApiService, useClass: AlfrescoApiMock },
{ provide: TranslationService, useClass: TranslationMock }, { provide: TranslationService, useClass: TranslationMock },
@ -112,7 +117,8 @@ import { ContentApiService } from '../services/content-api.service';
NodeActionsService, NodeActionsService,
NodePermissionService, NodePermissionService,
BrowsingFilesService, BrowsingFilesService,
ContentApiService ContentApiService,
ExtensionService
] ]
}) })
export class AppTestingModule {} export class AppTestingModule {}