[ACA-2064] support custom icons for extensions (#864)

* icon component, custom svg

* split components, fix modules

* simplify code

* universal icon component

* support custom icon registration

* update docs

* test fixes
This commit is contained in:
Denys Vuika 2018-12-07 19:09:45 +00:00 committed by GitHub
parent ec3eeb7a63
commit 99a8192b36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 505 additions and 229 deletions

View File

@ -24,6 +24,7 @@ You can create plugins that change, toggle, or extend the following areas:
- buttons
- "More actions" buttons
- Content metadata presets (used on `Properties` tab)
- Custom icons
Extensions can also:

42
docs/extending/icons.md Normal file
View File

@ -0,0 +1,42 @@
# Custom Icons
You can register and use custom `.svg` icons with toolbars, context menus, etc.
The icons are declared in the `features.icons` section, for example:
```json
{
"features": {
"icons": [
{
"id": "adf:join_library",
"value": "./assets/images/join-library.svg"
},
{
"id": "adf:move_file",
"value": "./assets/images/adf-move-file-24px.svg"
}
]
}
}
```
The `id` value must conform to the format `[namespace]:[name]`,
similar to that of the [Material Icon](https://material.angular.io/components/icon/api) component.
The icon file path should be relative to the deployed application root (or `index.html` file);
After that, you can use the icon id with other elements, for example:
```json
{
"id": "app.toolbar.move",
"order": 500,
"title": "APP.ACTIONS.MOVE",
"icon": "adf:move_file",
"actions": {
"click": "MOVE_NODES"
}
}
```
It is also possible to override the icon value or disable the entry from within external extensions.

View File

@ -1,38 +1,39 @@
- [Home](/)
- [Documentation](/#documentation)
- [How to contribute](/#how-to-contribute)
- [Documentation](/#documentation)
- [How to contribute](/#how-to-contribute)
- [App features](/features/)
- [User interface layout](/features/user-interface-layout)
- [Header](/features/header)
- [Side navigation](/features/side-navigation)
- [Document List Layout](/features/document-list-layout)
- [File Viewer](/features/file-viewer)
- [Info Drawer](/features/info-drawer)
- [Version Manager](/features/version-manager)
- [Search results](/features/search-results)
- [User interface layout](/features/user-interface-layout)
- [Header](/features/header)
- [Side navigation](/features/side-navigation)
- [Document List Layout](/features/document-list-layout)
- [File Viewer](/features/file-viewer)
- [Info Drawer](/features/info-drawer)
- [Version Manager](/features/version-manager)
- [Search results](/features/search-results)
- [Getting started](/getting-started/)
- [Prerequisites](/getting-started/prerequisites)
- [Building from source](/getting-started/building-from-source)
- [Internationalization (i18n)](/getting-started/internationalization)
- [CORS](/getting-started/cors)
- [Configuration](/getting-started/configuration)
- [Navigation](/getting-started/navigation)
- [Docker](/getting-started/docker)
- [Prerequisites](/getting-started/prerequisites)
- [Building from source](/getting-started/building-from-source)
- [Internationalization (i18n)](/getting-started/internationalization)
- [CORS](/getting-started/cors)
- [Configuration](/getting-started/configuration)
- [Navigation](/getting-started/navigation)
- [Docker](/getting-started/docker)
- [Extending](/extending/)
- [Extensibility features](/extending/extensibility-features)
- [Extension format](/extending/extension-format)
- [Routes](/extending/routes)
- [Components](/extending/components)
- [Actions](/extending/actions)
- [Application actions](/extending/application-actions)
- [Rules](/extending/rules)
- [Application features](/extending/application-features)
- [Registration](/extending/registration)
- [Creating custom evaluators](/extending/creating-custom-evaluators)
- [Tutorials](/extending/tutorials)
- [Redistributable libraries](/extending/redistributable-libraries)
- [Extensibility features](/extending/extensibility-features)
- [Extension format](/extending/extension-format)
- [Routes](/extending/routes)
- [Components](/extending/components)
- [Actions](/extending/actions)
- [Application actions](/extending/application-actions)
- [Rules](/extending/rules)
- [Application features](/extending/application-features)
- [Custom icons](/extending/icons)
- [Registration](/extending/registration)
- [Creating custom evaluators](/extending/creating-custom-evaluators)
- [Tutorials](/extending/tutorials)
- [Redistributable libraries](/extending/redistributable-libraries)
- [Tutorials](/tutorials/)
- [Introduction to extending ACA](/tutorials/introduction-to-extending)
- [Custom route with parameters](/tutorials/custom-route-with-parameters)
- [Dialog actions](/tutorials/dialog-actions)
- [Introduction to extending ACA](/tutorials/introduction-to-extending)
- [Custom route with parameters](/tutorials/custom-route-with-parameters)
- [Dialog actions](/tutorials/dialog-actions)
- [Get help](/help)

View File

@ -563,6 +563,24 @@
"type": "boolean"
}
}
},
"iconRef": {
"type": "object",
"required": ["id", "value"],
"properties": {
"id": {
"description": "Unique identifier. Must be in the format '[namespace]:[name]'.",
"type": "string"
},
"value": {
"description": "Icon path relative to the application root.",
"type": "string"
},
"disabled": {
"description": "Toggles the disabled state",
"type": "boolean"
}
}
}
},
@ -628,6 +646,12 @@
"description": "Application-specific features and extensions",
"type": "object",
"properties": {
"icons": {
"description": "Custom icons",
"type": "array",
"items": { "$ref": "#/definitions/iconRef" },
"minItems": 1
},
"header": {
"description": "Application header extensions",
"type": "array",

View File

@ -73,10 +73,6 @@ import { AppSearchResultsModule } from './components/search/search-results.modul
import { AppLoginModule } from './components/login/login.module';
import { AppHeaderModule } from './components/header/header.module';
import { environment } from '../environments/environment';
import { LibraryMembershipDirective } from './directives/library-membership.directive';
import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-library/toggle-join-library.component';
import { LibraryFavoriteDirective } from './directives/library-favorite.directive';
import { ToggleFavoriteLibraryComponent } from './components/toolbar/toggle-favorite-library/toggle-favorite-library.component';
import { AppDataService } from './services/data.service';
@NgModule({
@ -118,11 +114,7 @@ import { AppDataService } from './services/data.service';
LibrariesComponent,
FavoriteLibrariesComponent,
NodeVersionsDialogComponent,
LibraryDialogComponent,
LibraryMembershipDirective,
ToggleJoinLibraryComponent,
LibraryFavoriteDirective,
ToggleFavoriteLibraryComponent
LibraryDialogComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
@ -137,12 +129,7 @@ import { AppDataService } from './services/data.service';
}
}
],
entryComponents: [
LibraryDialogComponent,
NodeVersionsDialogComponent,
ToggleJoinLibraryComponent,
ToggleFavoriteLibraryComponent
],
entryComponents: [LibraryDialogComponent, NodeVersionsDialogComponent],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -34,9 +34,11 @@ import { LibraryStatusColumnComponent } from './library-status-column/library-st
import { LibraryRoleColumnComponent } from './library-role-column/library-role-column.component';
import { TrashcanNameColumnComponent } from './trashcan-name-column/trashcan-name-column.component';
import { DynamicColumnComponent } from './dynamic-column/dynamic-column.component';
import { IconComponent } from './icon/icon.component';
import { MatIconModule } from '@angular/material';
@NgModule({
imports: [CommonModule, CoreModule.forChild()],
imports: [CommonModule, CoreModule.forChild(), MatIconModule],
declarations: [
GenericErrorComponent,
LocationLinkComponent,
@ -45,7 +47,8 @@ import { DynamicColumnComponent } from './dynamic-column/dynamic-column.componen
LibraryStatusColumnComponent,
LibraryRoleColumnComponent,
TrashcanNameColumnComponent,
DynamicColumnComponent
DynamicColumnComponent,
IconComponent
],
exports: [
GenericErrorComponent,
@ -55,7 +58,8 @@ import { DynamicColumnComponent } from './dynamic-column/dynamic-column.componen
LibraryStatusColumnComponent,
LibraryRoleColumnComponent,
TrashcanNameColumnComponent,
DynamicColumnComponent
DynamicColumnComponent,
IconComponent
],
entryComponents: [
LocationLinkComponent,

View File

@ -0,0 +1,7 @@
<ng-container *ngIf="isCustom; else: default">
<mat-icon [svgIcon]="value"></mat-icon>
</ng-container>
<ng-template #default>
<mat-icon>{{ value }}</mat-icon>
</ng-template>

View File

@ -0,0 +1,4 @@
.adf-icon {
display: inline-flex;
vertical-align: middle;
}

View File

@ -0,0 +1,58 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Component,
Input,
ViewEncapsulation,
ChangeDetectionStrategy
} from '@angular/core';
@Component({
selector: 'adf-icon',
templateUrl: './icon.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'adf-icon' },
styleUrls: ['./icon.component.scss']
})
export class IconComponent {
private _value = '';
private _isCustom = false;
get value(): string {
return this._value;
}
@Input()
set value(value: string) {
this._value = value || 'settings';
this._isCustom = this._value.includes(':');
}
get isCustom(): boolean {
return this._isCustom;
}
}

View File

@ -1,39 +1,38 @@
<div class="aca-context-menu">
<ng-container [ngSwitch]="actionRef.type">
<ng-container [ngSwitch]="actionRef.type">
<ng-container *ngSwitchCase="'menu'">
<button mat-menu-item [id]="actionRef.id" [matMenuTriggerFor]="childMenu">
<adf-icon [value]="actionRef.icon"></adf-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
<ng-container *ngSwitchCase="'menu'">
<button
mat-menu-item
[id]="actionRef.id"
[matMenuTriggerFor]="childMenu">
<mat-icon color="primary">{{ actionRef.icon }}</mat-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu">
<ng-container *ngFor="let child of actionRef.children; trackBy: trackById">
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="actionRef.component"></adf-dynamic-component>
</ng-container>
<ng-container *ngSwitchDefault>
<button mat-menu-item
color="primary"
[id]="actionRef.id"
(click)="runAction()">
<mat-icon color="primary">{{ actionRef.icon }}</mat-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu">
<ng-container
*ngFor="let child of actionRef.children; trackBy: trackById"
>
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
</div>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="actionRef.component"></adf-dynamic-component>
</ng-container>
<ng-container *ngSwitchDefault>
<button
mat-menu-item
color="primary"
[id]="actionRef.id"
(click)="runAction()"
>
<adf-icon [value]="actionRef.icon"></adf-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
</ng-container>
</ng-container>
</div>

View File

@ -75,7 +75,9 @@ describe('ContextMenuComponent', () => {
fixture.detectChanges();
const buttonElement = fixture.nativeElement.querySelector('button');
expect(buttonElement.innerText.trim()).toBe(contextItem.title);
expect(buttonElement.querySelector('span').innerText.trim()).toBe(
contextItem.title
);
});
it('should not run action when entry has no click attribute defined', () => {

View File

@ -1,41 +1,50 @@
<button style="visibility: hidden" [matMenuTriggerFor]="rootMenu" #rootTriggerEl></button>
<button
style="visibility: hidden"
[matMenuTriggerFor]="rootMenu"
#rootTriggerEl
></button>
<mat-menu #rootMenu="matMenu"
class="aca-context-menu"
hasBackdrop="false"
acaContextMenuOutsideEvent
(clickOutside)="onClickOutsideEvent()">
<ng-container *ngFor="let entry of actions; trackBy: trackById" [ngSwitch]="entry.type">
<ng-container *ngSwitchDefault>
<button mat-menu-item
[id]="entry.id"
(click)="runAction(entry.actions.click)">
<mat-icon color="primary">{{ entry.icon }}</mat-icon>
<span>{{ entry.title | translate }}</span>
</button>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'menu'">
<button mat-menu-item
[id]="entry.id"
[matMenuTriggerFor]="childMenu">
<mat-icon color="primary">{{ entry.icon }}</mat-icon>
<span>{{ entry.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu">
<ng-container *ngFor="let child of entry.children; trackBy: trackById">
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
</ng-container>
<mat-menu
#rootMenu="matMenu"
class="aca-context-menu"
hasBackdrop="false"
acaContextMenuOutsideEvent
(clickOutside)="onClickOutsideEvent()"
>
<ng-container
*ngFor="let entry of actions; trackBy: trackById"
[ngSwitch]="entry.type"
>
<ng-container *ngSwitchDefault>
<button
mat-menu-item
[id]="entry.id"
(click)="runAction(entry.actions.click)"
>
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
<mat-divider></mat-divider>
</ng-container>
<ng-container *ngSwitchCase="'menu'">
<button mat-menu-item [id]="entry.id" [matMenuTriggerFor]="childMenu">
<adf-icon [value]="entry.icon"></adf-icon>
<span>{{ entry.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu">
<ng-container *ngFor="let child of entry.children; trackBy: trackById">
<app-context-menu-item [actionRef]="child"></app-context-menu-item>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<adf-dynamic-component [id]="entry.component"></adf-dynamic-component>
</ng-container>
</ng-container>
</mat-menu>

View File

@ -111,7 +111,9 @@ describe('ContextMenuComponent', () => {
.querySelectorAll('button');
expect(contextMenuElements.length).toBe(1);
expect(contextMenuElements[0].innerText).toBe(contextItem.title);
expect(contextMenuElements[0].querySelector('span').innerText).toBe(
contextItem.title
);
}));
it('should run action with provided action id', fakeAsync(() => {

View File

@ -38,6 +38,7 @@ import { ContextMenuComponent } from './context-menu.component';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { OutsideEventDirective } from './context-menu-outside-event.directive';
import { ContextMenuItemComponent } from './context-menu-item.component';
import { AppCommonModule } from '../common/common.module';
@NgModule({
imports: [
@ -47,6 +48,7 @@ import { ContextMenuItemComponent } from './context-menu-item.component';
MatButtonModule,
CoreExtensionsModule.forChild(),
CoreModule.forChild(),
AppCommonModule,
ExtensionsModule
],
declarations: [

View File

@ -44,8 +44,6 @@ import { currentFolder } from '../../../store/selectors/app.selectors';
import { AppStore } from '../../../store/states';
import { BreakpointObserver } from '@angular/cdk/layout';
import { SetSelectedNodesAction } from '../../../store/actions';
import { MatIconRegistry } from '@angular/material';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-layout',
@ -70,25 +68,14 @@ export class AppLayoutComponent implements OnInit, OnDestroy {
private minimizeConditions: string[] = ['search'];
private hideConditions: string[] = ['preview'];
customIcon: any = {
join_library: './assets/images/join-library.svg'
};
constructor(
protected store: Store<AppStore>,
private permission: NodePermissionService,
private router: Router,
private userPreferenceService: UserPreferencesService,
private appConfigService: AppConfigService,
private breakpointObserver: BreakpointObserver,
matIconRegistry: MatIconRegistry,
sanitizer: DomSanitizer
) {
matIconRegistry.addSvgIcon(
'join_library',
sanitizer.bypassSecurityTrustResourceUrl(this.customIcon['join_library'])
);
}
private breakpointObserver: BreakpointObserver
) {}
ngOnInit() {
this.isSmallScreen$ = this.breakpointObserver

View File

@ -37,10 +37,11 @@ import {
import { SetSelectedNodesAction } from '../../../store/actions/node.actions';
@Component({
selector: 'app-toggle-join-library',
selector: 'app-toggle-join-library-button',
template: `
<button
mat-menu-item
mat-icon-button
color="primary"
#membership="libraryMembership"
(toggle)="onToggleEvent($event)"
(error)="onErrorEvent($event)"
@ -54,36 +55,14 @@ import { SetSelectedNodesAction } from '../../../store/actions/node.actions';
<mat-icon *ngIf="(membership.isJoinRequested | async)">cancel</mat-icon>
<mat-icon
*ngIf="!(membership.isJoinRequested | async)"
svgIcon="join_library"
style="pointer-events: none;"
svgIcon="adf:join_library"
></mat-icon>
<span class="sideLabel">{{
(membership.isJoinRequested | async)
? ('APP.ACTIONS.CANCEL_JOIN' | translate)
: ('APP.ACTIONS.JOIN' | translate)
}}</span>
</button>
`,
styles: [
`
aca-toolbar-action .app-toggle-join-library .sideLabel {
display: none;
}
aca-toolbar-action .app-toggle-join-library .mat-menu-item mat-icon {
padding: 0;
margin: 0;
}
.adf-toolbar .app-toggle-join-library .mat-menu-item:hover {
background: none;
}
`
],
encapsulation: ViewEncapsulation.None,
host: { class: 'app-toggle-join-library' }
})
export class ToggleJoinLibraryComponent {
export class ToggleJoinLibraryButtonComponent {
selection$: Observable<SelectionState>;
constructor(

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
import { Component, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { ContentManagementService } from '../../../services/content-management.service';
import { ToggleJoinLibraryButtonComponent } from './toggle-join-library-button.component';
@Component({
selector: 'app-toggle-join-library-menu',
template: `
<button
mat-menu-item
#membership="libraryMembership"
(toggle)="onToggleEvent($event)"
(error)="onErrorEvent($event)"
[acaLibraryMembership]="(selection$ | async).library"
[attr.title]="
(membership.isJoinRequested | async)
? ('APP.ACTIONS.CANCEL_JOIN' | translate)
: ('APP.ACTIONS.JOIN' | translate)
"
>
<mat-icon *ngIf="(membership.isJoinRequested | async)">cancel</mat-icon>
<mat-icon
*ngIf="!(membership.isJoinRequested | async)"
svgIcon="adf:join_library"
></mat-icon>
<span>{{
(membership.isJoinRequested | async)
? ('APP.ACTIONS.CANCEL_JOIN' | translate)
: ('APP.ACTIONS.JOIN' | translate)
}}</span>
</button>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'app-toggle-join-library' }
})
export class ToggleJoinLibraryMenuComponent extends ToggleJoinLibraryButtonComponent {
constructor(store: Store<AppStore>, content: ContentManagementService) {
super(store, content);
}
}

View File

@ -23,7 +23,6 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ToggleJoinLibraryComponent } from './toggle-join-library.component';
import { of } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-core';
@ -36,10 +35,11 @@ import {
} from '../../../store/actions/snackbar.actions';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { ContentManagementService } from '../../../services/content-management.service';
import { ToggleJoinLibraryButtonComponent } from './toggle-join-library-button.component';
describe('ToggleJoinLibraryComponent', () => {
let component: ToggleJoinLibraryComponent;
let fixture: ComponentFixture<ToggleJoinLibraryComponent>;
let component: ToggleJoinLibraryButtonComponent;
let fixture: ComponentFixture<ToggleJoinLibraryButtonComponent>;
let alfrescoApi: AlfrescoApiService;
let contentManagementService: ContentManagementService;
let entry;
@ -59,7 +59,10 @@ describe('ToggleJoinLibraryComponent', () => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [ToggleJoinLibraryComponent, LibraryMembershipDirective],
declarations: [
ToggleJoinLibraryButtonComponent,
LibraryMembershipDirective
],
providers: [
{ provide: Store, useValue: storeMock },
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }
@ -67,7 +70,7 @@ describe('ToggleJoinLibraryComponent', () => {
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(ToggleJoinLibraryComponent);
fixture = TestBed.createComponent(ToggleJoinLibraryButtonComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
contentManagementService = TestBed.get(ContentManagementService);

View File

@ -1,15 +1,16 @@
<ng-container [ngSwitch]="type">
<ng-container *ngSwitchCase="'icon-button'">
<button
[id]="actionRef.id"
mat-icon-button
[color]="color"
[attr.title]="(actionRef.description || actionRef.title) | translate"
(click)="runAction()">
<mat-icon>{{ actionRef.icon }}</mat-icon>
</button>
</ng-container>
<ng-container *ngSwitchCase="'menu-item'">
<app-toolbar-menu-item [actionRef]="actionRef"></app-toolbar-menu-item>
</ng-container>
<ng-container *ngSwitchCase="'icon-button'">
<button
[id]="actionRef.id"
mat-icon-button
[color]="color"
[attr.title]="actionRef.description || actionRef.title | translate"
(click)="runAction()"
>
<adf-icon [value]="actionRef.icon"></adf-icon>
</button>
</ng-container>
<ng-container *ngSwitchCase="'menu-item'">
<app-toolbar-menu-item [actionRef]="actionRef"></app-toolbar-menu-item>
</ng-container>
</ng-container>

View File

@ -1,19 +1,21 @@
<ng-container [ngSwitch]="actionRef.type">
<ng-container *ngSwitchCase="'menu'">
<button
mat-menu-item
[disabled]="actionRef.disabled"
[matMenuTriggerFor]="childMenu">
<mat-icon>{{ actionRef.icon }}</mat-icon>
[matMenuTriggerFor]="childMenu"
>
<adf-icon [value]="actionRef.icon"></adf-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
<mat-menu #childMenu="matMenu" class="app-create-menu__sub-menu">
<ng-container *ngFor="let child of actionRef.children; trackBy: trackById">
<ng-container
*ngFor="let child of actionRef.children; trackBy: trackById"
>
<app-toolbar-menu-item [actionRef]="child"></app-toolbar-menu-item>
</ng-container>
</mat-menu>
</mat-menu>
</ng-container>
<ng-container *ngSwitchCase="'separator'">
@ -30,17 +32,15 @@
mat-menu-item
color="primary"
[disabled]="actionRef.disabled"
[attr.title]="(
actionRef.disabled
? actionRef['description-disabled']
: actionRef.description || actionRef.title
) | translate"
(click)="runAction()">
<mat-icon>{{ actionRef.icon }}</mat-icon>
[attr.title]="
(actionRef.disabled
? actionRef['description-disabled']
: actionRef.description || actionRef.title) | translate
"
(click)="runAction()"
>
<adf-icon [value]="actionRef.icon"></adf-icon>
<span>{{ actionRef.title | translate }}</span>
</button>
</ng-container>
</ng-container>

View File

@ -2,9 +2,10 @@
[id]="actionRef.id"
[color]="color"
mat-icon-button
[attr.title]="(actionRef.description || actionRef.title) | translate"
[matMenuTriggerFor]="menu">
<mat-icon>{{ actionRef.icon }}</mat-icon>
[attr.title]="actionRef.description || actionRef.title | translate"
[matMenuTriggerFor]="menu"
>
<adf-icon [value]="actionRef.icon"></adf-icon>
</button>
<mat-menu #menu="matMenu" [overlapTrigger]="false">

View File

@ -34,6 +34,11 @@ import { ToolbarActionComponent } from './toolbar-action/toolbar-action.componen
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { ToolbarMenuItemComponent } from './toolbar-menu-item/toolbar-menu-item.component';
import { ToolbarMenuComponent } from './toolbar-menu/toolbar-menu.component';
import { ToggleJoinLibraryButtonComponent } from './toggle-join-library/toggle-join-library-button.component';
import { ToggleJoinLibraryMenuComponent } from './toggle-join-library/toggle-join-library-menu.component';
import { DirectivesModule } from '../../directives/directives.module';
import { ToggleFavoriteLibraryComponent } from './toggle-favorite-library/toggle-favorite-library.component';
import { AppCommonModule } from '../common/common.module';
export function components() {
return [
@ -43,12 +48,21 @@ export function components() {
ToolbarButtonComponent,
ToolbarActionComponent,
ToolbarMenuItemComponent,
ToolbarMenuComponent
ToolbarMenuComponent,
ToggleJoinLibraryButtonComponent,
ToggleJoinLibraryMenuComponent,
ToggleFavoriteLibraryComponent
];
}
@NgModule({
imports: [CommonModule, CoreModule.forChild(), ExtensionsModule],
imports: [
CommonModule,
CoreModule.forChild(),
AppCommonModule,
ExtensionsModule,
DirectivesModule
],
declarations: components(),
exports: components(),
entryComponents: components()

View File

@ -27,13 +27,21 @@ import { NgModule } from '@angular/core';
import { ExperimentalDirective } from './experimental.directive';
import { DocumentListDirective } from './document-list.directive';
import { PaginationDirective } from './pagination.directive';
import { LibraryMembershipDirective } from './library-membership.directive';
import { LibraryFavoriteDirective } from './library-favorite.directive';
@NgModule({
declarations: [
export function directives() {
return [
ExperimentalDirective,
DocumentListDirective,
PaginationDirective
],
exports: [ExperimentalDirective, DocumentListDirective, PaginationDirective]
PaginationDirective,
LibraryMembershipDirective,
LibraryFavoriteDirective
];
}
@NgModule({
declarations: directives(),
exports: directives()
})
export class DirectivesModule {}

View File

@ -48,7 +48,8 @@ import { LibraryStatusColumnComponent } from '../components/common/library-statu
import { TrashcanNameColumnComponent } from '../components/common/trashcan-name-column/trashcan-name-column.component';
import { LocationLinkComponent } from '../components/common/location-link/location-link.component';
import { DocumentDisplayModeComponent } from '../components/toolbar/document-display-mode/document-display-mode.component';
import { ToggleJoinLibraryComponent } from '../components/toolbar/toggle-join-library/toggle-join-library.component';
import { ToggleJoinLibraryButtonComponent } from '../components/toolbar/toggle-join-library/toggle-join-library-button.component';
import { ToggleJoinLibraryMenuComponent } from '../components/toolbar/toggle-join-library/toggle-join-library-menu.component';
export function setupExtensions(service: AppExtensionService): Function {
return () => service.load();
@ -88,8 +89,9 @@ export class CoreExtensionsModule {
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
'app.toolbar.toggleFavorite': ToggleFavoriteComponent,
'app.toolbar.toggleFavoriteLibrary': ToggleFavoriteLibraryComponent,
'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryComponent,
'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryButtonComponent,
'app.toolbar.cardView': DocumentDisplayModeComponent,
'app.menu.toggleJoinLibrary': ToggleJoinLibraryMenuComponent,
'app.shared-link.toggleSharedLink': ToggleSharedComponent,
'app.columns.name': NameColumnComponent,
'app.columns.libraryName': LibraryNameColumnComponent,

View File

@ -26,6 +26,8 @@
import { Injectable, Type } from '@angular/core';
import { Store } from '@ngrx/store';
import { Route } from '@angular/router';
import { MatIconRegistry } from '@angular/material';
import { DomSanitizer } from '@angular/platform-browser';
import { AppStore } from '../store/states';
import { ruleContext } from '../store/selectors/app.selectors';
import { NodePermissionService } from '../services/node-permission.service';
@ -53,6 +55,7 @@ import {
import { AppConfigService } from '@alfresco/adf-core';
import { DocumentListPresetRef } from './document-list.extensions';
import { BehaviorSubject, Observable } from 'rxjs';
import { IconRef } from './icon.extensions';
@Injectable({
providedIn: 'root'
@ -105,11 +108,13 @@ export class AppExtensionService implements RuleContext {
references$: Observable<ExtensionRef[]>;
constructor(
private store: Store<AppStore>,
private loader: ExtensionLoaderService,
private extensions: ExtensionService,
protected store: Store<AppStore>,
protected loader: ExtensionLoaderService,
protected extensions: ExtensionService,
public permissions: NodePermissionService,
private appConfig: AppConfigService
protected appConfig: AppConfigService,
protected matIconRegistry: MatIconRegistry,
protected sanitizer: DomSanitizer
) {
this.references$ = this._references.asObservable();
@ -184,12 +189,37 @@ export class AppExtensionService implements RuleContext {
searchLibraries: this.getDocumentListPreset(config, 'search-libraries')
};
this.registerIcons(config);
const references = (config.$references || [])
.filter(entry => typeof entry === 'object')
.map(entry => <ExtensionRef>entry);
this._references.next(references);
}
protected registerIcons(config: ExtensionConfig) {
const icons: Array<IconRef> = this.loader
.getElements<IconRef>(config, 'features.icons')
.filter(entry => !entry.disabled);
for (const icon of icons) {
const [ns, id] = icon.id.split(':');
const value = icon.value;
if (!value) {
console.warn(`Missing icon value for "${icon.id}".`);
} else if (!ns || !id) {
console.warn(`Incorrect icon id format: "${icon.id}".`);
} else {
this.matIconRegistry.addSvgIconInNamespace(
ns,
id,
this.sanitizer.bypassSecurityTrustResourceUrl(value)
);
}
}
}
protected loadNavBar(config: ExtensionConfig): Array<NavBarGroupRef> {
return this.loader.getElements<NavBarGroupRef>(config, 'features.navbar');
}

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/>.
*/
import { ExtensionElement } from '@alfresco/adf-extensions';
export interface IconRef extends ExtensionElement {
value: string;
}

View File

@ -220,6 +220,16 @@
],
"features": {
"icons": [
{
"id": "adf:join_library",
"value": "./assets/images/join-library.svg"
},
{
"id": "adf:move_file",
"value": "./assets/images/adf-move-file-24px.svg"
}
],
"create": [
{
"id": "app.create.uploadFile",
@ -362,7 +372,7 @@
"id": "app.toolbar.share",
"order": 100,
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"icon": "link",
"actions": {
"click": "SHARE_NODE"
},
@ -374,7 +384,7 @@
"id": "app.toolbar.share.edit",
"order": 101,
"title": "APP.ACTIONS.SHARE_EDIT",
"icon": "share",
"icon": "link",
"actions": {
"click": "SHARE_NODE"
},
@ -386,7 +396,7 @@
"id": "app.toolbar.preview",
"order": 300,
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
"icon": "visibility",
"actions": {
"click": "VIEW_FILE"
},
@ -557,7 +567,7 @@
"id": "app.toolbar.move",
"order": 500,
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"icon": "adf:move_file",
"actions": {
"click": "MOVE_NODES"
},
@ -647,7 +657,7 @@
"id": "app.context.menu.preview",
"order": 300,
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
"icon": "visibility",
"actions": {
"click": "VIEW_FILE"
},
@ -714,7 +724,7 @@
"id": "app.context.menu.joinLibrary",
"type": "custom",
"order": 603,
"component": "app.toolbar.toggleJoinLibrary",
"component": "app.menu.toggleJoinLibrary",
"rules": {
"visible": "app.libraries.toolbar.canToggleJoin"
}
@ -752,7 +762,7 @@
"id": "app.context.menu.move",
"title": "APP.ACTIONS.MOVE",
"order": 800,
"icon": "library_books",
"icon": "adf:move_file",
"actions": {
"click": "MOVE_NODES"
},
@ -868,7 +878,7 @@
"id": "app.viewer.share",
"order": 200,
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"icon": "link",
"actions": {
"click": "SHARE_NODE"
},
@ -880,7 +890,7 @@
"id": "app.viewer.share.edit",
"order": 201,
"title": "APP.ACTIONS.SHARE_EDIT",
"icon": "share",
"icon": "link",
"actions": {
"click": "SHARE_NODE"
},
@ -962,7 +972,7 @@
"id": "app.viewer.move",
"order": 500,
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"icon": "adf:move_file",
"actions": {
"click": "MOVE_NODES"
},

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g id="Artboard_1"><g id="Page_1"><rect id="Fill_1" x="0" y="0" width="24" height="24" style="fill:none;"/><path id="Fill_2" d="M20,20l-16,0c-1.097,0 -2,-0.903 -2,-2l0.01,-12c0,-1.093 0.897,-1.995 1.99,-2l6,0l2,2l8,0c1.097,0 2,0.903 2,2l0,10c0,1.097 -0.903,2 -2,2Zm-9,-9l0,4l4,0l0,2l4,-4l-4,-4l0,2l-4,0Z" style="fill-rule:nonzero;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 794 B

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>join-library</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="0.0003 0 16 0 16 16 0.0003 16"></polygon>
<polygon id="path-3" points="0 0 15.9998 0 15.9998 16 0 16"></polygon>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -55,7 +55,12 @@
"check-type"
],
"directive-selector": [true, "attribute", "aca", "camelCase"],
"component-selector": [true, "element", ["app", "aca"], "kebab-case"],
"component-selector": [
true,
"element",
["app", "aca", "adf"],
"kebab-case"
],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": false,