mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
[ACS-8706] split context menu to allow injecting actions (#4203)
* ACS-8706 split context menu to allow injecting actions * ACS-8706 fix class naming, add context menu components unit tests * ACS-8706 add context menu service, effects and directive unit tests * ACS-8706 review remarks - redundant condition, directive unit tests * ACS-8706 improve unit testing approach, remove unnecessary class attributes * ACS-8706 documentation * ACS-8706 fix sonar issues * ACS-8706 replace takeUntil with takeUntilDestroyed * ACS-8706 fix sonar lint issue * ACS-8706 change incorrect import path
This commit is contained in:
parent
38e667b334
commit
71764b09e2
@ -26,3 +26,4 @@ This application simplifies the complexity of Content Management and provides co
|
||||
- [Search results](/features/search-results)
|
||||
- [Search forms](/features/search-forms)
|
||||
- [Application Hook](/extending/application-hook)
|
||||
- [Context Menu actions](context-menu-actions)
|
||||
|
69
docs/features/context-menu-actions.md
Normal file
69
docs/features/context-menu-actions.md
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
Title: Context Menu Actions
|
||||
---
|
||||
|
||||
# Context Menu Actions
|
||||
|
||||
Context Menu Component, appearing on right-clicking a document list item, contains Actions executable on particular file or folder. This entry describes two ways of populating Context Menu.
|
||||
|
||||
**Important:** Those two ways are ***mutually exclusive***.
|
||||
|
||||
## Default behavior
|
||||
|
||||
When using `acaContextActions` directive as shown below, Context Menu actions are loaded from `app.extensions.json` by default.
|
||||
|
||||
```html
|
||||
<adf-document-list
|
||||
#documentList
|
||||
acaContextActions>
|
||||
</adf-document-list>
|
||||
```
|
||||
|
||||
*Note:* To learn more, see [Extensibility features](../extending/extensibility-features.md) and [Extension format](../extending/extension-format.md).
|
||||
|
||||
## Injecting Context Menu Actions
|
||||
|
||||
In order to inject custom actions into Context Menu, assign an array of rules, formatted as described in [Extension format](../extending/extension-format.md), to an attribute of a Component using [Document List Component](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/docs/content-services/components/document-list.component.md).
|
||||
|
||||
```ts
|
||||
const contextMenuAction = [
|
||||
{
|
||||
"id": "custom.action.id",
|
||||
"title": "CUSTOM_ACTION",
|
||||
"order": 1,
|
||||
"icon": "adf:custom-icon",
|
||||
"actions": {
|
||||
"click": "CUSTOM_ACTION"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "show.custom.action"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "another.custom.action.id"
|
||||
|
||||
...
|
||||
}
|
||||
]
|
||||
|
||||
...
|
||||
|
||||
@Component({...})
|
||||
export class ComponentWithDocumentList {
|
||||
customContextMenuActions = contextMenuActions;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Next, pass them to `customActions` input of `acaContextActions` directive inside component's template.
|
||||
|
||||
```html
|
||||
<adf-document-list
|
||||
#documentList
|
||||
acaContextActions
|
||||
customActions="customContextMenuActions">
|
||||
</adf-document-list>
|
||||
```
|
||||
|
||||
*Note:* Refer to [Application Actions](../extending/application-actions.md) and [Rules](../extending/rules.md) for information on creating custom *"actions"* and *"rules"* for Context Menu actions.
|
@ -0,0 +1,108 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* 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
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||
import { ContentActionType } from '@alfresco/adf-extensions';
|
||||
import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
import { BaseContextMenuDirective } from './base-context-menu.directive';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component } from '@angular/core';
|
||||
import { AppTestingModule } from '../../testing/app-testing.module';
|
||||
import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-test-component',
|
||||
template: '<div acaContextMenuOutsideEvent (clickOutside)="onClickOutsideEvent()"></div>',
|
||||
standalone: true,
|
||||
imports: [OutsideEventDirective]
|
||||
})
|
||||
class TestComponent extends BaseContextMenuDirective {}
|
||||
|
||||
describe('BaseContextMenuComponent', () => {
|
||||
let contextMenuOverlayRef: ContextMenuOverlayRef;
|
||||
let extensionsService: AppExtensionService;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let component: TestComponent;
|
||||
|
||||
const contextItem = {
|
||||
type: ContentActionType.button,
|
||||
id: 'action-button',
|
||||
title: 'Test Button',
|
||||
actions: {
|
||||
click: 'TEST_EVENT'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule, TestComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ContextMenuOverlayRef,
|
||||
useValue: {
|
||||
close: jasmine.createSpy('close')
|
||||
}
|
||||
},
|
||||
BaseContextMenuDirective,
|
||||
OutsideEventDirective
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef);
|
||||
extensionsService = TestBed.inject(AppExtensionService);
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close context menu on Escape event', () => {
|
||||
fixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
|
||||
expect(contextMenuOverlayRef.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close context menu on click outside event', () => {
|
||||
fixture.debugElement.query(By.directive(OutsideEventDirective)).injector.get(OutsideEventDirective).clickOutside.emit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(contextMenuOverlayRef.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run action with provided action id and correct payload', () => {
|
||||
spyOn(extensionsService, 'runActionById');
|
||||
|
||||
component.runAction(contextItem);
|
||||
|
||||
expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click, {
|
||||
focusedElementOnCloseSelector: '.adf-context-menu-source'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return action id on trackByActionId', () => {
|
||||
const actionId = component.trackByActionId(0, contextItem);
|
||||
expect(actionId).toBe(contextItem.id);
|
||||
});
|
||||
});
|
@ -0,0 +1,68 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* 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
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { HostListener, ViewChild, Inject, Directive } from '@angular/core';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { ContentActionRef } from '@alfresco/adf-extensions';
|
||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
||||
import { Direction } from '@angular/cdk/bidi';
|
||||
import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
|
||||
@Directive()
|
||||
export class BaseContextMenuDirective {
|
||||
actions: Array<ContentActionRef> = [];
|
||||
|
||||
@ViewChild(MatMenuTrigger)
|
||||
trigger: MatMenuTrigger;
|
||||
|
||||
@HostListener('keydown.escape', ['$event'])
|
||||
handleKeydownEscape(event: KeyboardEvent) {
|
||||
if (event && this.contextMenuOverlayRef) {
|
||||
this.contextMenuOverlayRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly contextMenuOverlayRef: ContextMenuOverlayRef,
|
||||
protected extensions: AppExtensionService,
|
||||
@Inject(CONTEXT_MENU_DIRECTION) public direction: Direction
|
||||
) {}
|
||||
|
||||
onClickOutsideEvent() {
|
||||
if (this.contextMenuOverlayRef) {
|
||||
this.contextMenuOverlayRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
runAction(contentActionRef: ContentActionRef) {
|
||||
this.extensions.runActionById(contentActionRef.actions.click, {
|
||||
focusedElementOnCloseSelector: '.adf-context-menu-source'
|
||||
});
|
||||
}
|
||||
|
||||
trackByActionId(_: number, obj: ContentActionRef): string {
|
||||
return obj.id;
|
||||
}
|
||||
}
|
@ -35,7 +35,6 @@ import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
describe('ContextMenuComponent', () => {
|
||||
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||
let component: ContextMenuComponent;
|
||||
let contextMenuOverlayRef: ContextMenuOverlayRef;
|
||||
let extensionsService: AppExtensionService;
|
||||
|
||||
const contextItem = {
|
||||
@ -49,7 +48,7 @@ describe('ContextMenuComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ContextMenuComponent, AppTestingModule],
|
||||
imports: [AppTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ContextMenuOverlayRef,
|
||||
@ -70,7 +69,6 @@ describe('ContextMenuComponent', () => {
|
||||
fixture = TestBed.createComponent(ContextMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef);
|
||||
extensionsService = TestBed.inject(AppExtensionService);
|
||||
|
||||
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem]));
|
||||
@ -78,30 +76,17 @@ describe('ContextMenuComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close context menu on Escape event', () => {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
expect(contextMenuOverlayRef.close).toHaveBeenCalled();
|
||||
it('should load context menu actions on init', () => {
|
||||
expect(component.actions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render defined context menu actions items', async () => {
|
||||
component.ngAfterViewInit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const contextMenuElements = document.body.querySelector('.aca-context-menu')?.querySelectorAll('button');
|
||||
const actionButtonLabel: HTMLElement = contextMenuElements?.[0].querySelector('[data-automation-id="action-button-label"]');
|
||||
const actionButtonLabel: HTMLElement = contextMenuElements?.[0].querySelector(`[data-automation-id="${contextItem.id}-label"]`);
|
||||
|
||||
expect(contextMenuElements?.length).toBe(1);
|
||||
expect(actionButtonLabel.innerText).toBe(contextItem.title);
|
||||
});
|
||||
|
||||
it('should run action with provided action id and correct payload', () => {
|
||||
spyOn(extensionsService, 'runActionById');
|
||||
|
||||
component.runAction(contextItem);
|
||||
|
||||
expect(extensionsService.runActionById).toHaveBeenCalledWith(contextItem.actions.click, {
|
||||
focusedElementOnCloseSelector: '.adf-context-menu-source'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -22,11 +22,9 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit, Inject } from '@angular/core';
|
||||
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ContentActionRef, DynamicExtensionComponent } from '@alfresco/adf-extensions';
|
||||
import { Component, ViewEncapsulation, OnInit, AfterViewInit, Inject, inject, DestroyRef } from '@angular/core';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
|
||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
||||
import { Direction } from '@angular/cdk/bidi';
|
||||
@ -37,6 +35,8 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||
import { IconComponent } from '@alfresco/adf-core';
|
||||
import { ContextMenuItemComponent } from './context-menu-item.component';
|
||||
import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
||||
import { BaseContextMenuDirective } from './base-context-menu.directive';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@ -58,57 +58,23 @@ import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
actions: Array<ContentActionRef> = [];
|
||||
export class ContextMenuComponent extends BaseContextMenuDirective implements OnInit, AfterViewInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
@ViewChild(MatMenuTrigger)
|
||||
trigger: MatMenuTrigger;
|
||||
|
||||
@HostListener('document:keydown.Escape', ['$event'])
|
||||
handleKeydownEscape(event: KeyboardEvent) {
|
||||
if (event) {
|
||||
if (this.contextMenuOverlayRef) {
|
||||
this.contextMenuOverlayRef.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private contextMenuOverlayRef: ContextMenuOverlayRef,
|
||||
private extensions: AppExtensionService,
|
||||
@Inject(CONTEXT_MENU_DIRECTION) public direction: Direction
|
||||
) {}
|
||||
|
||||
onClickOutsideEvent() {
|
||||
if (this.contextMenuOverlayRef) {
|
||||
this.contextMenuOverlayRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
runAction(contentActionRef: ContentActionRef) {
|
||||
this.extensions.runActionById(contentActionRef.actions.click, {
|
||||
focusedElementOnCloseSelector: '.adf-context-menu-source'
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
constructor(contextMenuOverlayRef: ContextMenuOverlayRef, extensions: AppExtensionService, @Inject(CONTEXT_MENU_DIRECTION) direction: Direction) {
|
||||
super(contextMenuOverlayRef, extensions, direction);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.extensions
|
||||
.getAllowedContextMenuActions()
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((actions) => (this.actions = actions));
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((actions) => {
|
||||
this.actions = actions;
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.trigger.openMenu(), 0);
|
||||
}
|
||||
|
||||
trackByActionId(_: number, obj: ContentActionRef): string {
|
||||
return obj.id;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import { ContextMenuService } from './context-menu.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContextMenuComponent } from './context-menu.component';
|
||||
import { ContextmenuOverlayConfig } from './interfaces';
|
||||
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
|
||||
|
||||
describe('ContextMenuService', () => {
|
||||
let contextMenuService: ContextMenuService;
|
||||
@ -39,6 +40,17 @@ describe('ContextMenuService', () => {
|
||||
let injector: Injector;
|
||||
let userPreferencesService: UserPreferencesService;
|
||||
|
||||
const customActionMock: ContentActionRef[] = [
|
||||
{
|
||||
type: ContentActionType.default,
|
||||
id: 'action',
|
||||
title: 'action',
|
||||
actions: {
|
||||
click: 'event'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const overlayConfig: ContextmenuOverlayConfig = {
|
||||
hasBackdrop: false,
|
||||
backdropClass: '',
|
||||
@ -93,4 +105,20 @@ describe('ContextMenuService', () => {
|
||||
|
||||
expect(document.body.querySelector('div[dir="rtl"]')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should render custom context menu component', () => {
|
||||
contextMenuService = new ContextMenuService(injector, overlay, userPreferencesService);
|
||||
|
||||
contextMenuService.open(overlayConfig, customActionMock);
|
||||
|
||||
expect(document.querySelector('aca-custom-context-menu')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should not render custom context menu when no custom actions are provided', () => {
|
||||
contextMenuService = new ContextMenuService(injector, overlay, userPreferencesService);
|
||||
|
||||
contextMenuService.open(overlayConfig, []);
|
||||
|
||||
expect(document.querySelector('aca-custom-context-menu')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
@ -31,6 +31,9 @@ import { ContextmenuOverlayConfig } from './interfaces';
|
||||
import { UserPreferencesService } from '@alfresco/adf-core';
|
||||
import { Directionality } from '@angular/cdk/bidi';
|
||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
||||
import { CONTEXT_MENU_CUSTOM_ACTIONS } from './custom-context-menu-actions.token';
|
||||
import { ContentActionRef } from '@alfresco/adf-extensions';
|
||||
import { CustomContextMenuComponent } from './custom-context-menu.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -44,11 +47,14 @@ export class ContextMenuService {
|
||||
});
|
||||
}
|
||||
|
||||
open(config: ContextmenuOverlayConfig): ContextMenuOverlayRef {
|
||||
open(config: ContextmenuOverlayConfig, customActions?: ContentActionRef[]): ContextMenuOverlayRef {
|
||||
const overlay = this.createOverlay(config);
|
||||
const overlayRef = new ContextMenuOverlayRef(overlay);
|
||||
|
||||
this.attachDialogContainer(overlay, overlayRef);
|
||||
if (customActions?.length) {
|
||||
this.attachCustomDialogContainer(overlay, overlayRef, customActions);
|
||||
} else {
|
||||
this.attachDialogContainer(overlay, overlayRef);
|
||||
}
|
||||
|
||||
return overlayRef;
|
||||
}
|
||||
@ -60,7 +66,6 @@ export class ContextMenuService {
|
||||
|
||||
private attachDialogContainer(overlay: OverlayRef, contextmenuOverlayRef: ContextMenuOverlayRef): ContextMenuComponent {
|
||||
const injector = this.createInjector(contextmenuOverlayRef);
|
||||
|
||||
const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector);
|
||||
const containerRef: ComponentRef<ContextMenuComponent> = overlay.attach(containerPortal);
|
||||
|
||||
@ -77,6 +82,29 @@ export class ContextMenuService {
|
||||
});
|
||||
}
|
||||
|
||||
private attachCustomDialogContainer(
|
||||
overlay: OverlayRef,
|
||||
contextmenuOverlayRef: ContextMenuOverlayRef,
|
||||
customActions: ContentActionRef[]
|
||||
): CustomContextMenuComponent {
|
||||
const injector = this.createCustomInjector(contextmenuOverlayRef, customActions);
|
||||
const containerPortal = new ComponentPortal(CustomContextMenuComponent, null, injector);
|
||||
const containerRef: ComponentRef<CustomContextMenuComponent> = overlay.attach(containerPortal);
|
||||
|
||||
return containerRef.instance;
|
||||
}
|
||||
|
||||
private createCustomInjector(contextmenuOverlayRef: ContextMenuOverlayRef, customActions: ContentActionRef[]): Injector {
|
||||
return Injector.create({
|
||||
parent: this.injector,
|
||||
providers: [
|
||||
{ provide: ContextMenuOverlayRef, useValue: contextmenuOverlayRef },
|
||||
{ provide: CONTEXT_MENU_DIRECTION, useValue: this.direction },
|
||||
{ provide: CONTEXT_MENU_CUSTOM_ACTIONS, useValue: customActions }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig {
|
||||
const { x, y } = config.source;
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* 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
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const CONTEXT_MENU_CUSTOM_ACTIONS = new InjectionToken('CONTEXT_MENU_CUSTOM_ACTIONS');
|
@ -0,0 +1,99 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* 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
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../testing/app-testing.module';
|
||||
import { CustomContextMenuComponent } from './custom-context-menu.component';
|
||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||
import { ContentActionType } from '@alfresco/adf-extensions';
|
||||
import { CONTEXT_MENU_CUSTOM_ACTIONS } from './custom-context-menu-actions.token';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { initialState } from '@alfresco/aca-shared';
|
||||
|
||||
describe('ContextMenuComponent', () => {
|
||||
let fixture: ComponentFixture<CustomContextMenuComponent>;
|
||||
let component: CustomContextMenuComponent;
|
||||
|
||||
const contextMenuActionsMock = [
|
||||
{
|
||||
type: ContentActionType.button,
|
||||
id: 'action1',
|
||||
title: 'action1',
|
||||
actions: {
|
||||
click: 'event1'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: ContentActionType.button,
|
||||
id: 'action2',
|
||||
title: 'action2',
|
||||
actions: {
|
||||
click: 'event2'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ContextMenuOverlayRef,
|
||||
useValue: {
|
||||
close: jasmine.createSpy('close')
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: Store,
|
||||
useValue: MockStore
|
||||
},
|
||||
provideMockStore({ initialState }),
|
||||
{
|
||||
provide: CONTEXT_MENU_CUSTOM_ACTIONS,
|
||||
useValue: contextMenuActionsMock
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(CustomContextMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should set context menu actions from Injection Token', () => {
|
||||
expect(component.actions.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should render defined context menu actions items', async () => {
|
||||
await fixture.whenStable();
|
||||
|
||||
const contextMenuElements = document.body.querySelector('.aca-context-menu')?.querySelectorAll('button');
|
||||
const actionButtonLabel: HTMLElement = contextMenuElements?.[0].querySelector(`[data-automation-id="${contextMenuActionsMock[0].id}-label"]`);
|
||||
|
||||
expect(contextMenuElements?.length).toBe(2);
|
||||
expect(actionButtonLabel.innerText).toBe(contextMenuActionsMock[0].title);
|
||||
});
|
||||
});
|
@ -0,0 +1,75 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* 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
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { AfterViewInit, Component, Inject, ViewEncapsulation } from '@angular/core';
|
||||
import { Direction } from '@angular/cdk/bidi';
|
||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||
import { AppExtensionService } from '@alfresco/aca-shared';
|
||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
||||
import { ContentActionRef, DynamicExtensionComponent } from '@alfresco/adf-extensions';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { ContextMenuItemComponent } from './context-menu-item.component';
|
||||
import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { IconComponent } from '@alfresco/adf-core';
|
||||
import { CONTEXT_MENU_CUSTOM_ACTIONS } from './custom-context-menu-actions.token';
|
||||
import { BaseContextMenuDirective } from './base-context-menu.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-custom-context-menu',
|
||||
templateUrl: './context-menu.component.html',
|
||||
styleUrls: ['./context-menu.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
MatMenuModule,
|
||||
MatDividerModule,
|
||||
ContextMenuItemComponent,
|
||||
OutsideEventDirective,
|
||||
IconComponent,
|
||||
DynamicExtensionComponent
|
||||
],
|
||||
host: {
|
||||
class: 'aca-context-menu-holder'
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true
|
||||
})
|
||||
export class CustomContextMenuComponent extends BaseContextMenuDirective implements AfterViewInit {
|
||||
constructor(
|
||||
contextMenuOverlayRef: ContextMenuOverlayRef,
|
||||
extensions: AppExtensionService,
|
||||
@Inject(CONTEXT_MENU_DIRECTION) direction: Direction,
|
||||
@Inject(CONTEXT_MENU_CUSTOM_ACTIONS) customActions: ContentActionRef[]
|
||||
) {
|
||||
super(contextMenuOverlayRef, extensions, direction);
|
||||
this.actions = customActions;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.trigger.openMenu(), 0);
|
||||
}
|
||||
}
|
@ -71,7 +71,6 @@ import { SearchAiEffects } from './effects/search-ai.effects';
|
||||
TemplateEffects,
|
||||
ContextMenuEffects,
|
||||
SearchAiEffects,
|
||||
ContextMenuEffects,
|
||||
SnackbarEffects,
|
||||
RouterEffects
|
||||
])
|
||||
|
@ -27,10 +27,22 @@ import { AppTestingModule } from '../../testing/app-testing.module';
|
||||
import { ContextMenuEffects } from './contextmenu.effects';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { ContextMenu, CustomContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { ContextMenuService } from '../../components/context-menu/context-menu.service';
|
||||
import { OverlayModule, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ContextMenuOverlayRef } from '../../components/context-menu/context-menu-overlay';
|
||||
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
|
||||
|
||||
const actionPayloadMock: ContentActionRef[] = [
|
||||
{
|
||||
type: ContentActionType.default,
|
||||
id: 'action',
|
||||
title: 'action',
|
||||
actions: {
|
||||
click: 'event'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
describe('ContextMenuEffects', () => {
|
||||
let store: Store<any>;
|
||||
@ -62,4 +74,22 @@ describe('ContextMenuEffects', () => {
|
||||
store.dispatch(new ContextMenu(new MouseEvent('click')));
|
||||
expect(overlayRefMock.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open custom context menu on customContextMenu$ action', () => {
|
||||
store.dispatch(new CustomContextMenu(new MouseEvent('click'), actionPayloadMock));
|
||||
expect(contextMenuService.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not open custom context menu on customContextMenu$ action if no action provided', () => {
|
||||
store.dispatch(new CustomContextMenu(new MouseEvent('click'), []));
|
||||
expect(contextMenuService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close custom context menu if a new one is opened', () => {
|
||||
store.dispatch(new CustomContextMenu(new MouseEvent('click'), actionPayloadMock));
|
||||
expect(contextMenuService.open).toHaveBeenCalled();
|
||||
|
||||
store.dispatch(new CustomContextMenu(new MouseEvent('click'), actionPayloadMock));
|
||||
expect(overlayRefMock.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ContextMenu, ContextMenuActionTypes } from '@alfresco/aca-shared/store';
|
||||
import { ContextMenu, ContextMenuActionTypes, CustomContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { map } from 'rxjs/operators';
|
||||
@ -55,4 +55,28 @@ export class ContextMenuEffects {
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
customContextMenu$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType<CustomContextMenu>(ContextMenuActionTypes.CustomContextMenu),
|
||||
map((action) => {
|
||||
if (action.payload?.length) {
|
||||
if (this.overlayRef) {
|
||||
this.overlayRef.close();
|
||||
}
|
||||
this.overlayRef = this.contextMenuService.open(
|
||||
{
|
||||
source: action.event,
|
||||
hasBackdrop: false,
|
||||
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||
panelClass: 'cdk-overlay-pane'
|
||||
},
|
||||
action.payload
|
||||
);
|
||||
}
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
}
|
||||
|
@ -23,9 +23,21 @@
|
||||
*/
|
||||
|
||||
import { ContextActionsDirective } from './contextmenu.directive';
|
||||
import { ContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { ContextMenu, CustomContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
|
||||
const customActionsMock: ContentActionRef[] = [
|
||||
{
|
||||
type: ContentActionType.default,
|
||||
id: 'action',
|
||||
title: 'action',
|
||||
actions: {
|
||||
click: 'event'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
describe('ContextActionsDirective', () => {
|
||||
let directive: ContextActionsDirective;
|
||||
|
||||
@ -45,24 +57,6 @@ describe('ContextActionsDirective', () => {
|
||||
expect(directive.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call service to render context menu', fakeAsync(() => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'adf-datatable-cell adf-datatable-cell--text adf-datatable-row';
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(el);
|
||||
const target = fragment.querySelector('div');
|
||||
const mouseEventMock: any = { preventDefault: () => {}, target };
|
||||
|
||||
directive.ngOnInit();
|
||||
|
||||
directive.onContextMenuEvent(mouseEventMock);
|
||||
|
||||
tick(500);
|
||||
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(new ContextMenu(mouseEventMock));
|
||||
}));
|
||||
|
||||
it('should not call service to render context menu if the datatable is empty', fakeAsync(() => {
|
||||
storeMock.dispatch.calls.reset();
|
||||
const el = document.createElement('div');
|
||||
@ -82,4 +76,34 @@ describe('ContextActionsDirective', () => {
|
||||
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
describe('Context Menu rendering', () => {
|
||||
let mouseEventMock: any;
|
||||
beforeEach(() => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'adf-datatable-cell adf-datatable-cell--text adf-datatable-row';
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(el);
|
||||
const target = fragment.querySelector('div');
|
||||
mouseEventMock = { preventDefault: () => {}, target };
|
||||
});
|
||||
|
||||
it('should call service to render context menu', fakeAsync(() => {
|
||||
directive.ngOnInit();
|
||||
directive.onContextMenuEvent(mouseEventMock);
|
||||
tick(500);
|
||||
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(new ContextMenu(mouseEventMock));
|
||||
}));
|
||||
|
||||
it('should call service to render custom context menu if custom actions are provided', fakeAsync(() => {
|
||||
directive.customActions = customActionsMock;
|
||||
directive.ngOnInit();
|
||||
directive.onContextMenuEvent(mouseEventMock);
|
||||
tick(500);
|
||||
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(new CustomContextMenu(mouseEventMock, customActionsMock));
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -26,7 +26,8 @@ import { Directive, HostListener, Input, OnInit, OnDestroy } from '@angular/core
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore, ContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { AppStore, ContextMenu, CustomContextMenu } from '@alfresco/aca-shared/store';
|
||||
import { ContentActionRef } from '@alfresco/adf-extensions';
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
@ -41,6 +42,9 @@ export class ContextActionsDirective implements OnInit, OnDestroy {
|
||||
@Input('acaContextEnable')
|
||||
enabled = true;
|
||||
|
||||
@Input()
|
||||
customActions: ContentActionRef[] = [];
|
||||
|
||||
@HostListener('contextmenu', ['$event'])
|
||||
onContextMenuEvent(event: MouseEvent) {
|
||||
if (event) {
|
||||
@ -59,7 +63,11 @@ export class ContextActionsDirective implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.execute$.pipe(debounceTime(300), takeUntil(this.onDestroy$)).subscribe((event: MouseEvent) => {
|
||||
this.store.dispatch(new ContextMenu(event));
|
||||
if (this.customActions?.length) {
|
||||
this.store.dispatch(new CustomContextMenu(event, this.customActions));
|
||||
} else {
|
||||
this.store.dispatch(new ContextMenu(event));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,5 +23,6 @@
|
||||
*/
|
||||
|
||||
export enum ContextMenuActionTypes {
|
||||
ContextMenu = 'CONTEXT_MENU'
|
||||
ContextMenu = 'CONTEXT_MENU',
|
||||
CustomContextMenu = 'CUSTOM_CONTEXT_MENU'
|
||||
}
|
||||
|
@ -24,9 +24,16 @@
|
||||
|
||||
import { Action } from '@ngrx/store';
|
||||
import { ContextMenuActionTypes } from './context-menu-action-types';
|
||||
import { ContentActionRef } from '@alfresco/adf-extensions';
|
||||
|
||||
export class ContextMenu implements Action {
|
||||
readonly type = ContextMenuActionTypes.ContextMenu;
|
||||
|
||||
constructor(public event: MouseEvent) {}
|
||||
}
|
||||
|
||||
export class CustomContextMenu implements Action {
|
||||
readonly type = ContextMenuActionTypes.CustomContextMenu;
|
||||
|
||||
constructor(public event: MouseEvent, public payload: ContentActionRef[] = []) {}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user