Skip to content

Commit

Permalink
feat(workbench/view): enable translation of built-in context menu
Browse files Browse the repository at this point in the history
To translate built-in menu items, provide a function instead of a string. The function can call `inject` to get any required dependencies, or use `toSignal` to convert an observable to a signal.

Example:
```ts
import {provideWorkbench} from '@scion/workbench';
import {inject} from '@angular/core';
import {Observable} from 'rxjs';
import {toSignal} from '@angular/core/rxjs-interop';

provideWorkbench({
  viewMenuItems: {
    close: {
      // Returns a string literal or signal.
      text: () => inject(TranslateService).translate('close_tab'),
    },
    closeAll: {
      // Returns a signal, converting the observable to a signal.
      text: () => toSignal(inject(TranslateService).translate$('close_all_tabs'), {requireSync: true}),
    },
  },
});
```
> The `TranslateService` is illustrative and not part of the Workbench API
  • Loading branch information
Marcarrian authored and danielwiehl committed Sep 11, 2024
1 parent b0829b3 commit 9bfdf74
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 28 deletions.
2 changes: 1 addition & 1 deletion projects/scion/e2e-testing/src/view-tab-context-menu.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {Locator} from '@playwright/test';
export class ViewTabContextMenuPO {

public readonly menuItems = {
closeTab: new ContextMenuItem(this.locator.locator('button.e2e-close-tab')),
closeTab: new ContextMenuItem(this.locator.locator('button.e2e-close')),
closeAll: new ContextMenuItem(this.locator.locator('button.e2e-close-all-tabs')),
moveToNewWindow: new ContextMenuItem(this.locator.locator('button.e2e-move-to-new-window')),
moveView: new ContextMenuItem(this.locator.locator('button.e2e-move-view')),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2022 Swiss Federal Railways
* Copyright (c) 2018-2024 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -8,20 +8,27 @@
* SPDX-License-Identifier: EPL-2.0
*/

import {Component, Inject, InjectionToken} from '@angular/core';
import {Component, inject, InjectionToken, isSignal, signal, Signal} from '@angular/core';

export const TEXT = new InjectionToken<string>('TEXT');
export const TEXT = new InjectionToken<string | (() => string | Signal<string>)>('TEXT');

/**
* Component which renders text injected via {@link TEXT} injection token.
*/
@Component({
selector: 'wb-text',
template: '{{text}}',
template: '{{text()}}',
standalone: true,
})
export class TextComponent {

constructor(@Inject(TEXT) public text: string) {
protected text = coerceText(inject(TEXT));
}

function coerceText(textOrFn: string | (() => string | Signal<string>)): Signal<string> {
if (typeof textOrFn === 'function') {
const text = textOrFn();
return isSignal(text) ? text : signal(text);
}
return signal(textOrFn);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ export class ViewMenuService {

constructor() {
// Registers built-in menu items added to the context menu of every view tab.
this.registerCloseViewMenuItem();
this.registerCloseOtherViewsMenuItem();
this.registerCloseAllViewsMenuItem();
this.registerCloseViewsToTheRightMenuItem();
this.registerCloseViewsToTheLeftMenuItem();
this.registerCloseMenuItem();
this.registerCloseOtherTabsMenuItem();
this.registerCloseAllTabsMenuItem();
this.registerCloseRightTabsMenuItem();
this.registerCloseLeftTabsMenuItem();
this.registerMoveRightMenuItem();
this.registerMoveLeftMenuItem();
this.registerMoveUpMenuItem();
Expand Down Expand Up @@ -147,8 +147,8 @@ export class ViewMenuService {
});
}

private registerCloseViewMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close tab', group: 'close', accelerator: ['ctrl', 'k'], cssClass: 'e2e-close-tab'};
private registerCloseMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close', group: 'close', accelerator: ['ctrl', 'k'], cssClass: 'e2e-close'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.close;
const config = {...defaults, ...appConfig};

Expand All @@ -168,8 +168,8 @@ export class ViewMenuService {
});
}

private registerCloseOtherViewsMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close other tabs', group: 'close', accelerator: ['ctrl', 'shift', 'k']};
private registerCloseOtherTabsMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close other tabs', group: 'close', accelerator: ['ctrl', 'shift', 'k'], cssClass: ' e2e-close-other-tabs'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeOthers;
const config = {...defaults, ...appConfig};

Expand All @@ -189,7 +189,7 @@ export class ViewMenuService {
});
}

private registerCloseAllViewsMenuItem(): void {
private registerCloseAllTabsMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close all tabs', group: 'close', accelerator: ['ctrl', 'shift', 'alt', 'k'], cssClass: 'e2e-close-all-tabs'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeAll;
const config = {...defaults, ...appConfig};
Expand All @@ -209,8 +209,8 @@ export class ViewMenuService {
});
}

private registerCloseViewsToTheRightMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the right', group: 'close'};
private registerCloseRightTabsMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the right', group: 'close', cssClass: 'e2e-close-right-tabs'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeToTheRight;
const config = {...defaults, ...appConfig};

Expand All @@ -230,8 +230,8 @@ export class ViewMenuService {
});
}

private registerCloseViewsToTheLeftMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the left', group: 'close'};
private registerCloseLeftTabsMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the left', group: 'close', cssClass: 'e2e-close-left-tabs'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeToTheLeft;
const config = {...defaults, ...appConfig};

Expand All @@ -252,7 +252,7 @@ export class ViewMenuService {
}

private registerMoveRightMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Move right', group: 'move', accelerator: ['ctrl', 'alt', 'end']};
const defaults: MenuItemConfig = {visible: true, text: 'Move right', group: 'move', accelerator: ['ctrl', 'alt', 'end'], cssClass: 'e2e-move-right'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveRight;
const config = {...defaults, ...appConfig};

Expand All @@ -273,7 +273,7 @@ export class ViewMenuService {
}

private registerMoveLeftMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Move left', group: 'move'};
const defaults: MenuItemConfig = {visible: true, text: 'Move left', group: 'move', cssClass: 'e2e-move-left'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveLeft;
const config = {...defaults, ...appConfig};

Expand All @@ -294,7 +294,7 @@ export class ViewMenuService {
}

private registerMoveUpMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Move up', group: 'move'};
const defaults: MenuItemConfig = {visible: true, text: 'Move up', group: 'move', cssClass: 'e2e-move-up'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveUp;
const config = {...defaults, ...appConfig};

Expand All @@ -315,7 +315,7 @@ export class ViewMenuService {
}

private registerMoveDownMenuItem(): void {
const defaults: MenuItemConfig = {visible: true, text: 'Move down', group: 'move'};
const defaults: MenuItemConfig = {visible: true, text: 'Move down', group: 'move', cssClass: 'e2e-move-down'};
const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveDown;
const config = {...defaults, ...appConfig};

Expand Down
222 changes: 222 additions & 0 deletions projects/scion/workbench/src/lib/view/view.menu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Copyright (c) 2018-2024 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {provideRouter} from '@angular/router';
import {styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util';
import {By} from '@angular/platform-browser';
import {Component, DebugElement, signal} from '@angular/core';
import {expect} from '../testing/jasmine/matcher/custom-matchers.definition';
import {provideWorkbenchForTest} from '../testing/workbench.provider';
import {WorkbenchComponent} from '../workbench.component';
import {WorkbenchRouter} from '../routing/workbench-router.service';
import {of} from 'rxjs';
import {toSignal} from '@angular/core/rxjs-interop';

describe('View Menu', () => {

it('should display configured text (string)', async () => {
TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest({
viewMenuItems: {
close: {text: 'close-testee'},
closeOthers: {text: 'closeOthers-testee'},
closeAll: {text: 'closeAll-testee'},
closeToTheRight: {text: 'closeToTheRight-testee'},
closeToTheLeft: {text: 'closeToTheLeft-testee'},
moveUp: {text: 'moveUp-testee'},
moveRight: {text: 'moveRight-testee'},
moveDown: {text: 'moveDown-testee'},
moveLeft: {text: 'moveLeft-testee'},
moveToNewWindow: {text: 'moveToNewWindow-testee'},
},
}),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});
const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Navigate to "path/to/view".
const workbenchRouter = TestBed.inject(WorkbenchRouter);
await workbenchRouter.navigate(['path/to/view'], {target: 'view.100'});

// Open view context menu.
const contextMenu = await openViewContextMenu(fixture, {viewId: 'view.100'});

expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close'})).toEqual('close-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-other-tabs'})).toEqual('closeOthers-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-all-tabs'})).toEqual('closeAll-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-right-tabs'})).toEqual('closeToTheRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-left-tabs'})).toEqual('closeToTheLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-up'})).toEqual('moveUp-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-right'})).toEqual('moveRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-down'})).toEqual('moveDown-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-left'})).toEqual('moveLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-to-new-window'})).toEqual('moveToNewWindow-testee');
});

it('should display configured text (() => string))', async () => {
TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest({
viewMenuItems: {
close: {text: () => 'close-testee'},
closeOthers: {text: () => 'closeOthers-testee'},
closeAll: {text: () => 'closeAll-testee'},
closeToTheRight: {text: () => 'closeToTheRight-testee'},
closeToTheLeft: {text: () => 'closeToTheLeft-testee'},
moveUp: {text: () => 'moveUp-testee'},
moveRight: {text: () => 'moveRight-testee'},
moveDown: {text: () => 'moveDown-testee'},
moveLeft: {text: () => 'moveLeft-testee'},
moveToNewWindow: {text: () => 'moveToNewWindow-testee'},
},
}),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});
const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Navigate to "path/to/view".
const workbenchRouter = TestBed.inject(WorkbenchRouter);
await workbenchRouter.navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Open view context menu.
const contextMenu = await openViewContextMenu(fixture, {viewId: 'view.100'});

expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close'})).toEqual('close-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-other-tabs'})).toEqual('closeOthers-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-all-tabs'})).toEqual('closeAll-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-right-tabs'})).toEqual('closeToTheRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-left-tabs'})).toEqual('closeToTheLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-up'})).toEqual('moveUp-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-right'})).toEqual('moveRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-down'})).toEqual('moveDown-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-left'})).toEqual('moveLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-to-new-window'})).toEqual('moveToNewWindow-testee');
});

it('should display configured text (() => Signal))', async () => {
TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest({
viewMenuItems: {
close: {text: () => signal('close-testee')},
closeOthers: {text: () => signal('closeOthers-testee')},
closeAll: {text: () => signal('closeAll-testee')},
closeToTheRight: {text: () => signal('closeToTheRight-testee')},
closeToTheLeft: {text: () => signal('closeToTheLeft-testee')},
moveUp: {text: () => signal('moveUp-testee')},
moveRight: {text: () => signal('moveRight-testee')},
moveDown: {text: () => signal('moveDown-testee')},
moveLeft: {text: () => signal('moveLeft-testee')},
moveToNewWindow: {text: () => signal('moveToNewWindow-testee')},
},
}),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});
const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Navigate to "path/to/view".
const workbenchRouter = TestBed.inject(WorkbenchRouter);
await workbenchRouter.navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Open view context menu.
const contextMenu = await openViewContextMenu(fixture, {viewId: 'view.100'});

expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close'})).toEqual('close-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-other-tabs'})).toEqual('closeOthers-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-all-tabs'})).toEqual('closeAll-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-right-tabs'})).toEqual('closeToTheRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-left-tabs'})).toEqual('closeToTheLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-up'})).toEqual('moveUp-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-right'})).toEqual('moveRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-down'})).toEqual('moveDown-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-left'})).toEqual('moveLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-to-new-window'})).toEqual('moveToNewWindow-testee');
});

it('should display text provided as observable (() => Signal))', async () => {
TestBed.configureTestingModule({
providers: [
provideWorkbenchForTest({
viewMenuItems: {
close: {text: () => toSignal(of('close-testee'), {requireSync: true})},
closeOthers: {text: () => toSignal(of('closeOthers-testee'), {requireSync: true})},
closeAll: {text: () => toSignal(of('closeAll-testee'), {requireSync: true})},
closeToTheRight: {text: () => toSignal(of('closeToTheRight-testee'), {requireSync: true})},
closeToTheLeft: {text: () => toSignal(of('closeToTheLeft-testee'), {requireSync: true})},
moveUp: {text: () => toSignal(of('moveUp-testee'), {requireSync: true})},
moveRight: {text: () => toSignal(of('moveRight-testee'), {requireSync: true})},
moveDown: {text: () => toSignal(of('moveDown-testee'), {requireSync: true})},
moveLeft: {text: () => toSignal(of('moveLeft-testee'), {requireSync: true})},
moveToNewWindow: {text: () => toSignal(of('moveToNewWindow-testee'), {requireSync: true})},
},
}),
provideRouter([
{path: 'path/to/view', component: SpecViewComponent},
]),
],
});
const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent));
await waitForInitialWorkbenchLayout();

// Navigate to "path/to/view".
const workbenchRouter = TestBed.inject(WorkbenchRouter);
await workbenchRouter.navigate(['path/to/view'], {target: 'view.100'});
await waitUntilStable();

// Open view context menu.
const contextMenu = await openViewContextMenu(fixture, {viewId: 'view.100'});

expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close'})).toEqual('close-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-other-tabs'})).toEqual('closeOthers-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-all-tabs'})).toEqual('closeAll-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-right-tabs'})).toEqual('closeToTheRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-close-left-tabs'})).toEqual('closeToTheLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-up'})).toEqual('moveUp-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-right'})).toEqual('moveRight-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-down'})).toEqual('moveDown-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-left'})).toEqual('moveLeft-testee');
expect(getMenuItemText(contextMenu, {cssClass: 'e2e-move-to-new-window'})).toEqual('moveToNewWindow-testee');
});
});

@Component({
selector: 'spec-view',
template: 'SpecViewComponent',
standalone: true,
})
class SpecViewComponent {
}

async function openViewContextMenu(fixture: ComponentFixture<unknown>, locator: {viewId: string}): Promise<DebugElement> {
const viewTabElement = fixture.debugElement.query(By.css(`wb-view-tab[data-viewid="${locator.viewId}"]`));
viewTabElement.nativeElement.dispatchEvent(new MouseEvent('contextmenu'));
await waitUntilStable();
return fixture.debugElement.parent!.query(By.css(`div.cdk-overlay-pane wb-view-menu`));
}

function getMenuItemText(contextMenu: DebugElement, locator: {cssClass: string}): string {
return contextMenu.query(By.css(`button.menu-item.${locator.cssClass} > div.text`)).nativeElement.innerText;
}
Loading

0 comments on commit 9bfdf74

Please sign in to comment.