Skip to content

Commit

Permalink
[ACS-8706] split context menu to allow injecting actions (#4203)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
g-jaskowski authored Nov 8, 2024
1 parent 38e667b commit 71764b0
Show file tree
Hide file tree
Showing 18 changed files with 642 additions and 95 deletions.
1 change: 1 addition & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 69 additions & 0 deletions docs/features/context-menu-actions.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -49,7 +48,7 @@ describe('ContextMenuComponent', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContextMenuComponent, AppTestingModule],
imports: [AppTestingModule],
providers: [
{
provide: ContextMenuOverlayRef,
Expand All @@ -70,38 +69,24 @@ describe('ContextMenuComponent', () => {
fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance;

contextMenuOverlayRef = TestBed.inject(ContextMenuOverlayRef);
extensionsService = TestBed.inject(AppExtensionService);

spyOn(extensionsService, 'getAllowedContextMenuActions').and.returnValue(of([contextItem]));

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'
});
});
});
Loading

0 comments on commit 71764b0

Please sign in to comment.