Skip to content

Commit

Permalink
feat(workbench-client/popup): allow providing a microfrontend for dis…
Browse files Browse the repository at this point in the history
…play in a workbench popup

A micro application can provide a microfrontend for display in a workbench popup. Popup microfrontends are contributed in the form of popup capabilities. A popup is a visual workbench component for displaying content above other content.

Unlike views, popups are not part of the persistent workbench navigation, meaning that popups do not survive a page reload.
  • Loading branch information
danielwiehl authored and Marcarrian committed Jan 25, 2021
1 parent 9f1b69b commit bc23e65
Show file tree
Hide file tree
Showing 48 changed files with 3,706 additions and 24 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ jobs:
project: workbench-client
- name: view
project: workbench-client
- name: popup
project: workbench-client
- name: popup-params
project: workbench-client
- name: popup-router
project: workbench-client
- name: popup-size
project: workbench-client
steps:
- uses: actions/checkout@v2
- name: 'Downloading dist/workbench and dist/workbench-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,40 @@ export class ActivatorModule {
},
},
);

// Register view to open a workbench popup.
await this._manifestService.registerCapability(
{
type: WorkbenchCapabilities.View,
qualifier: {
component: 'popup',
app,
},
description: '[e2e] Allows opening a microfrontend in a workbench popup',
private: false,
properties: {
path: 'test-popup',
pinToStartPage: true,
title: 'Workbench Popup',
heading,
cssClass: 'e2e-test-popup',
},
});

// Register the popup microfrontend.
await this._manifestService.registerCapability(
{
type: WorkbenchCapabilities.Popup,
qualifier: {
component: 'popup',
app,
},
description: '[e2e] Provides access to the workbench popup object',
private: false,
properties: {
path: 'popup',
cssClass: 'e2e-test-popup',
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const routes: Routes = [
path: 'test-view',
loadChildren: (): any => import('./view-page/view-page.module').then(m => m.ViewPageModule),
},
{
path: 'test-popup',
loadChildren: (): any => import('./popup-opener-page/popup-opener-page.module').then(m => m.PopupOpenerPageModule),
},
{
path: 'popup',
loadChildren: (): any => import('./popup-page/popup-page.module').then(m => m.PopupPageModule),
},
{
path: 'register-workbench-capability',
loadChildren: (): any => import('./register-workbench-capability-page/register-workbench-capability-page.module').then(m => m.RegisterWorkbenchCapabilityPageModule),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<ng-container [formGroup]="form">
<section>
<sci-form-field label="Qualifier">
<sci-params-enter [paramsFormArray]="$any(form.get(QUALIFIER))" [addable]="true" [removable]="true" class="e2e-qualifier"></sci-params-enter>
</sci-form-field>

<sci-form-field label="Params">
<sci-params-enter [paramsFormArray]="$any(form.get(PARAMS))" [addable]="true" [removable]="true" class="e2e-params"></sci-params-enter>
</sci-form-field>

<sci-form-field label="Align">
<select [formControlName]="ALIGN" class="e2e-align">
<option value="east">east</option>
<option value="west">west</option>
<option value="north">north</option>
<option value="south">south</option>
<option value="">&lt;default&gt;</option>
</select>
</sci-form-field>
</section>

<sci-accordion class="anchor e2e-anchor" variant="solid" [formGroupName]="ANCHOR">
<ng-template sciAccordionItem [panel]="panel_anchor">
<header>Anchor</header>
</ng-template>
<ng-template #panel_anchor>
<sci-form-field label="Binding">
<select [formControlName]="BINDING" class="e2e-anchor">
<option value="element">Element</option>
<option value="coordinate">Coordinate</option>
</select>
</sci-form-field>
<ng-container *ngIf="form.get([ANCHOR, BINDING]).value === 'coordinate'">
<sci-form-field label="X">
<input [formControlName]="X" class="e2e-anchor-x">
</sci-form-field>
<sci-form-field label="Y">
<input [formControlName]="Y" class="e2e-anchor-y">
</sci-form-field>
<sci-form-field label="Width">
<input [formControlName]="WIDTH" class="e2e-anchor-width">
</sci-form-field>
<sci-form-field label="Height">
<input [formControlName]="HEIGHT" class="e2e-anchor-height">
</sci-form-field>
</ng-container>
</ng-template>
</sci-accordion>

<sci-accordion [formGroupName]="CLOSE_STRATEGY" variant="solid" class="e2e-close-strategy">
<ng-template sciAccordionItem [panel]="panel_close_strategy">
<header>Close Strategy</header>
</ng-template>
<ng-template #panel_close_strategy>
<sci-form-field label="onFocusLost">
<sci-checkbox [formControlName]="ON_FOCUS_LOST" class="e2e-close-on-focus-lost"></sci-checkbox>
</sci-form-field>

<sci-form-field label="onEscape">
<sci-checkbox [formControlName]="ON_ESCAPE" class="e2e-close-on-escape"></sci-checkbox>
</sci-form-field>
</ng-template>
</sci-accordion>
</ng-container>

<button (click)="onPopupOpen()" class="open e2e-open" [disabled]="form.invalid" #open_button>
Open popup
</button>

<output class="return-value e2e-return-value" *ngIf="returnValue">
{{returnValue}}
</output>

<output class="popup-error e2e-popup-error" *ngIf="popupError">
{{popupError}}
</output>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@use 'sci-toolkit-styles' as sci-toolkit-styles;

:host {
display: grid;
grid-auto-rows: max-content;
row-gap: 1em;
padding: 1em;

> section {
display: grid;
grid-row-gap: .5em;
border: 1px solid var(--sci-color-P400);
border-radius: 5px;
padding: 1em;
}

> sci-accordion header {
font-weight: bold;
}

> button.open {
justify-self: center;
}

> output.return-value {
border: 1px solid var(--sci-color-accent);
background-color: var(--sci-color-A100);
border-radius: 3px;
padding: 1em;
}

> output.popup-error {
border: 1px solid var(--sci-color-warn);
background-color: var(--sci-color-W100);
border-radius: 3px;
padding: 1em;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (c) 2018-2019 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 { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { CloseStrategy, WorkbenchPopupService } from '@scion/workbench-client';
import { SciParamsEnterComponent } from '@scion/toolkit.internal/widgets';
import { undefinedIfEmpty } from '../util/util';
import { defer, Observable } from 'rxjs';
import { PopupOrigin } from '@scion/workbench';
import { map, startWith } from 'rxjs/operators';

const QUALIFIER = 'qualifier';
const PARAMS = 'params';
const ANCHOR = 'anchor';
const BINDING = 'binding';
const ALIGN = 'align';
const CLOSE_STRATEGY = 'closeStrategy';
const ON_FOCUS_LOST = 'onFocusLost';
const ON_ESCAPE = 'onEscape';
const X = 'x';
const Y = 'y';
const HEIGHT = 'height';
const WIDTH = 'width';

@Component({
selector: 'app-popup-opener-page',
templateUrl: './popup-opener-page.component.html',
styleUrls: ['./popup-opener-page.component.scss'],
})
export class PopupOpenerPageComponent implements AfterViewInit {

public readonly QUALIFIER = QUALIFIER;
public readonly PARAMS = PARAMS;
public readonly ANCHOR = ANCHOR;
public readonly BINDING = BINDING;
public readonly ALIGN = ALIGN;
public readonly CLOSE_STRATEGY = CLOSE_STRATEGY;
public readonly ON_FOCUS_LOST = ON_FOCUS_LOST;
public readonly ON_ESCAPE = ON_ESCAPE;
public readonly HEIGHT = HEIGHT;
public readonly WIDTH = WIDTH;
public readonly X = X;
public readonly Y = Y;

private _coordinateAnchor$: Observable<PopupOrigin>;

public form: FormGroup;

public popupError: string;
public returnValue: string;

@ViewChild('open_button', {static: true})
private _openButton: ElementRef<HTMLButtonElement>;

constructor(private _host: ElementRef<HTMLElement>,
private _popupService: WorkbenchPopupService,
formBuilder: FormBuilder) {
this.form = formBuilder.group({
[QUALIFIER]: formBuilder.array([
new FormGroup({
paramName: new FormControl('component'),
paramValue: new FormControl('popup'),
}),
new FormGroup({
paramName: new FormControl('app'),
paramValue: new FormControl('app1'),
},
)], Validators.required),
[PARAMS]: formBuilder.array([]),
[ANCHOR]: formBuilder.group({
[BINDING]: formBuilder.control('element', Validators.required),
[X]: formBuilder.control('0'),
[Y]: formBuilder.control('0'),
[WIDTH]: formBuilder.control('0'),
[HEIGHT]: formBuilder.control('0'),
}),
[ALIGN]: formBuilder.control(''),
[CLOSE_STRATEGY]: formBuilder.group({
[ON_FOCUS_LOST]: formBuilder.control(true),
[ON_ESCAPE]: formBuilder.control(true),
}),
});

this._coordinateAnchor$ = defer(() => this.form.get(ANCHOR)
.valueChanges
.pipe(
startWith(this.form.get(ANCHOR).value as object),
map(formValue => ({
x: Number(formValue[X]),
y: Number(formValue[Y]),
width: Number(formValue[WIDTH]),
height: Number(formValue[HEIGHT]),
}),
),
));
}

public ngAfterViewInit(): void {
const {left, top, width, height} = this._host.nativeElement.getBoundingClientRect();
this.form.get(ANCHOR).patchValue({
[X]: left + width / 2,
[Y]: top + height / 2,
});
}

public async onPopupOpen(): Promise<void> {
const qualifier = SciParamsEnterComponent.toParamsDictionary(this.form.get(QUALIFIER) as FormArray);
const params = SciParamsEnterComponent.toParamsDictionary(this.form.get(PARAMS) as FormArray);

this.popupError = null;
this.returnValue = null;

await this._popupService.open<string>(qualifier, {
params,
anchor: this.form.get([ANCHOR, BINDING]).value === 'element' ? this._openButton.nativeElement : this._coordinateAnchor$,
align: this.form.get(ALIGN).value || undefined,
closeStrategy: undefinedIfEmpty<CloseStrategy>({
onFocusLost: this.form.get([CLOSE_STRATEGY, ON_FOCUS_LOST]).value ?? undefined,
onEscape: this.form.get([CLOSE_STRATEGY, ON_ESCAPE]).value ?? undefined,
}),
})
.then(result => this.returnValue = result)
.catch(error => this.popupError = error ?? 'Popup was closed with an error');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018-2019 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SciAccordionModule, SciCheckboxModule, SciFormFieldModule, SciParamsEnterModule } from '@scion/toolkit.internal/widgets';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { PopupOpenerPageComponent } from './popup-opener-page.component';

const routes: Routes = [
{path: '', component: PopupOpenerPageComponent},
];

@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forChild(routes),
SciFormFieldModule,
SciCheckboxModule,
SciAccordionModule,
SciParamsEnterModule,
],
declarations: [
PopupOpenerPageComponent,
],
})
export class PopupOpenerPageModule {
}
Loading

0 comments on commit bc23e65

Please sign in to comment.