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 results](/features/search-results)
|
||||||
- [Search forms](/features/search-forms)
|
- [Search forms](/features/search-forms)
|
||||||
- [Application Hook](/extending/application-hook)
|
- [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', () => {
|
describe('ContextMenuComponent', () => {
|
||||||
let fixture: ComponentFixture<ContextMenuComponent>;
|
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||||
let component: ContextMenuComponent;
|
let component: ContextMenuComponent;
|
||||||
let contextMenuOverlayRef: ContextMenuOverlayRef;
|
|
||||||
let extensionsService: AppExtensionService;
|
let extensionsService: AppExtensionService;
|
||||||
|
|
||||||
const contextItem = {
|
const contextItem = {
|
||||||
@ -49,7 +48,7 @@ describe('ContextMenuComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ContextMenuComponent, AppTestingModule],
|
imports: [AppTestingModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ContextMenuOverlayRef,
|
provide: ContextMenuOverlayRef,
|
||||||
@ -70,7 +69,6 @@ describe('ContextMenuComponent', () => {
|
|||||||
fixture = TestBed.createComponent(ContextMenuComponent);
|
fixture = TestBed.createComponent(ContextMenuComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef);
|
|
||||||
extensionsService = TestBed.inject(AppExtensionService);
|
extensionsService = TestBed.inject(AppExtensionService);
|
||||||
|
|
||||||
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem]));
|
spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem]));
|
||||||
@ -78,30 +76,17 @@ describe('ContextMenuComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close context menu on Escape event', () => {
|
it('should load context menu actions on init', () => {
|
||||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
expect(component.actions.length).toBe(1);
|
||||||
expect(contextMenuOverlayRef.close).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render defined context menu actions items', async () => {
|
it('should render defined context menu actions items', async () => {
|
||||||
component.ngAfterViewInit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
|
||||||
const contextMenuElements = document.body.querySelector('.aca-context-menu')?.querySelectorAll('button');
|
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(contextMenuElements?.length).toBe(1);
|
||||||
expect(actionButtonLabel.innerText).toBe(contextItem.title);
|
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/>.
|
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit, Inject } from '@angular/core';
|
import { Component, ViewEncapsulation, OnInit, AfterViewInit, Inject, inject, DestroyRef } from '@angular/core';
|
||||||
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { Subject } from 'rxjs';
|
import { DynamicExtensionComponent } from '@alfresco/adf-extensions';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
import { ContentActionRef, DynamicExtensionComponent } from '@alfresco/adf-extensions';
|
|
||||||
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
import { ContextMenuOverlayRef } from './context-menu-overlay';
|
||||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
||||||
import { Direction } from '@angular/cdk/bidi';
|
import { Direction } from '@angular/cdk/bidi';
|
||||||
@ -37,6 +35,8 @@ import { MatDividerModule } from '@angular/material/divider';
|
|||||||
import { IconComponent } from '@alfresco/adf-core';
|
import { IconComponent } from '@alfresco/adf-core';
|
||||||
import { ContextMenuItemComponent } from './context-menu-item.component';
|
import { ContextMenuItemComponent } from './context-menu-item.component';
|
||||||
import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
||||||
|
import { BaseContextMenuDirective } from './base-context-menu.directive';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -58,57 +58,23 @@ import { OutsideEventDirective } from './context-menu-outside-event.directive';
|
|||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class ContextMenuComponent extends BaseContextMenuDirective implements OnInit, AfterViewInit {
|
||||||
private onDestroy$: Subject<boolean> = new Subject<boolean>();
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
actions: Array<ContentActionRef> = [];
|
|
||||||
|
|
||||||
@ViewChild(MatMenuTrigger)
|
constructor(contextMenuOverlayRef: ContextMenuOverlayRef, extensions: AppExtensionService, @Inject(CONTEXT_MENU_DIRECTION) direction: Direction) {
|
||||||
trigger: MatMenuTrigger;
|
super(contextMenuOverlayRef, extensions, direction);
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.extensions
|
this.extensions
|
||||||
.getAllowedContextMenuActions()
|
.getAllowedContextMenuActions()
|
||||||
.pipe(takeUntil(this.onDestroy$))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((actions) => (this.actions = actions));
|
.subscribe((actions) => {
|
||||||
|
this.actions = actions;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
setTimeout(() => this.trigger.openMenu(), 0);
|
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 { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ContextMenuComponent } from './context-menu.component';
|
import { ContextMenuComponent } from './context-menu.component';
|
||||||
import { ContextmenuOverlayConfig } from './interfaces';
|
import { ContextmenuOverlayConfig } from './interfaces';
|
||||||
|
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
|
||||||
|
|
||||||
describe('ContextMenuService', () => {
|
describe('ContextMenuService', () => {
|
||||||
let contextMenuService: ContextMenuService;
|
let contextMenuService: ContextMenuService;
|
||||||
@ -39,6 +40,17 @@ describe('ContextMenuService', () => {
|
|||||||
let injector: Injector;
|
let injector: Injector;
|
||||||
let userPreferencesService: UserPreferencesService;
|
let userPreferencesService: UserPreferencesService;
|
||||||
|
|
||||||
|
const customActionMock: ContentActionRef[] = [
|
||||||
|
{
|
||||||
|
type: ContentActionType.default,
|
||||||
|
id: 'action',
|
||||||
|
title: 'action',
|
||||||
|
actions: {
|
||||||
|
click: 'event'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const overlayConfig: ContextmenuOverlayConfig = {
|
const overlayConfig: ContextmenuOverlayConfig = {
|
||||||
hasBackdrop: false,
|
hasBackdrop: false,
|
||||||
backdropClass: '',
|
backdropClass: '',
|
||||||
@ -93,4 +105,20 @@ describe('ContextMenuService', () => {
|
|||||||
|
|
||||||
expect(document.body.querySelector('div[dir="rtl"]')).not.toBe(null);
|
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 { UserPreferencesService } from '@alfresco/adf-core';
|
||||||
import { Directionality } from '@angular/cdk/bidi';
|
import { Directionality } from '@angular/cdk/bidi';
|
||||||
import { CONTEXT_MENU_DIRECTION } from './direction.token';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
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 overlay = this.createOverlay(config);
|
||||||
const overlayRef = new ContextMenuOverlayRef(overlay);
|
const overlayRef = new ContextMenuOverlayRef(overlay);
|
||||||
|
if (customActions?.length) {
|
||||||
this.attachDialogContainer(overlay, overlayRef);
|
this.attachCustomDialogContainer(overlay, overlayRef, customActions);
|
||||||
|
} else {
|
||||||
|
this.attachDialogContainer(overlay, overlayRef);
|
||||||
|
}
|
||||||
|
|
||||||
return overlayRef;
|
return overlayRef;
|
||||||
}
|
}
|
||||||
@ -60,7 +66,6 @@ export class ContextMenuService {
|
|||||||
|
|
||||||
private attachDialogContainer(overlay: OverlayRef, contextmenuOverlayRef: ContextMenuOverlayRef): ContextMenuComponent {
|
private attachDialogContainer(overlay: OverlayRef, contextmenuOverlayRef: ContextMenuOverlayRef): ContextMenuComponent {
|
||||||
const injector = this.createInjector(contextmenuOverlayRef);
|
const injector = this.createInjector(contextmenuOverlayRef);
|
||||||
|
|
||||||
const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector);
|
const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector);
|
||||||
const containerRef: ComponentRef<ContextMenuComponent> = overlay.attach(containerPortal);
|
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 {
|
private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig {
|
||||||
const { x, y } = config.source;
|
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,
|
TemplateEffects,
|
||||||
ContextMenuEffects,
|
ContextMenuEffects,
|
||||||
SearchAiEffects,
|
SearchAiEffects,
|
||||||
ContextMenuEffects,
|
|
||||||
SnackbarEffects,
|
SnackbarEffects,
|
||||||
RouterEffects
|
RouterEffects
|
||||||
])
|
])
|
||||||
|
@ -27,10 +27,22 @@ import { AppTestingModule } from '../../testing/app-testing.module';
|
|||||||
import { ContextMenuEffects } from './contextmenu.effects';
|
import { ContextMenuEffects } from './contextmenu.effects';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { Store } from '@ngrx/store';
|
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 { ContextMenuService } from '../../components/context-menu/context-menu.service';
|
||||||
import { OverlayModule, OverlayRef } from '@angular/cdk/overlay';
|
import { OverlayModule, OverlayRef } from '@angular/cdk/overlay';
|
||||||
import { ContextMenuOverlayRef } from '../../components/context-menu/context-menu-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', () => {
|
describe('ContextMenuEffects', () => {
|
||||||
let store: Store<any>;
|
let store: Store<any>;
|
||||||
@ -62,4 +74,22 @@ describe('ContextMenuEffects', () => {
|
|||||||
store.dispatch(new ContextMenu(new MouseEvent('click')));
|
store.dispatch(new ContextMenu(new MouseEvent('click')));
|
||||||
expect(overlayRefMock.close).toHaveBeenCalled();
|
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/>.
|
* 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 { inject, Injectable } from '@angular/core';
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
@ -55,4 +55,28 @@ export class ContextMenuEffects {
|
|||||||
),
|
),
|
||||||
{ dispatch: false }
|
{ 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 { 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';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
const customActionsMock: ContentActionRef[] = [
|
||||||
|
{
|
||||||
|
type: ContentActionType.default,
|
||||||
|
id: 'action',
|
||||||
|
title: 'action',
|
||||||
|
actions: {
|
||||||
|
click: 'event'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
describe('ContextActionsDirective', () => {
|
describe('ContextActionsDirective', () => {
|
||||||
let directive: ContextActionsDirective;
|
let directive: ContextActionsDirective;
|
||||||
|
|
||||||
@ -45,24 +57,6 @@ describe('ContextActionsDirective', () => {
|
|||||||
expect(directive.execute).not.toHaveBeenCalled();
|
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(() => {
|
it('should not call service to render context menu if the datatable is empty', fakeAsync(() => {
|
||||||
storeMock.dispatch.calls.reset();
|
storeMock.dispatch.calls.reset();
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
@ -82,4 +76,34 @@ describe('ContextActionsDirective', () => {
|
|||||||
|
|
||||||
expect(storeMock.dispatch).not.toHaveBeenCalled();
|
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 { debounceTime, takeUntil } from 'rxjs/operators';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { Store } from '@ngrx/store';
|
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({
|
@Directive({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -41,6 +42,9 @@ export class ContextActionsDirective implements OnInit, OnDestroy {
|
|||||||
@Input('acaContextEnable')
|
@Input('acaContextEnable')
|
||||||
enabled = true;
|
enabled = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
customActions: ContentActionRef[] = [];
|
||||||
|
|
||||||
@HostListener('contextmenu', ['$event'])
|
@HostListener('contextmenu', ['$event'])
|
||||||
onContextMenuEvent(event: MouseEvent) {
|
onContextMenuEvent(event: MouseEvent) {
|
||||||
if (event) {
|
if (event) {
|
||||||
@ -59,7 +63,11 @@ export class ContextActionsDirective implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.execute$.pipe(debounceTime(300), takeUntil(this.onDestroy$)).subscribe((event: MouseEvent) => {
|
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 {
|
export enum ContextMenuActionTypes {
|
||||||
ContextMenu = 'CONTEXT_MENU'
|
ContextMenu = 'CONTEXT_MENU',
|
||||||
|
CustomContextMenu = 'CUSTOM_CONTEXT_MENU'
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,16 @@
|
|||||||
|
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { ContextMenuActionTypes } from './context-menu-action-types';
|
import { ContextMenuActionTypes } from './context-menu-action-types';
|
||||||
|
import { ContentActionRef } from '@alfresco/adf-extensions';
|
||||||
|
|
||||||
export class ContextMenu implements Action {
|
export class ContextMenu implements Action {
|
||||||
readonly type = ContextMenuActionTypes.ContextMenu;
|
readonly type = ContextMenuActionTypes.ContextMenu;
|
||||||
|
|
||||||
constructor(public event: MouseEvent) {}
|
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