[PM-2198] Async simple configurable dialogs (#5411)

Implements a new functionality for simple configurable dialogs that allows you to set an acceptAction which triggers a pending state. To use this set acceptAction to an async method, and it will be executed on accept prior to closing the dialog.
This commit is contained in:
Oscar Hinton 2023-08-11 13:20:47 +02:00 committed by GitHub
parent 2187db2153
commit 4b1570b0b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 190 deletions

View File

@ -48,4 +48,10 @@ export type SimpleDialogOptions = {
/** Whether or not the user can use escape or clicking the backdrop to close the dialog */ /** Whether or not the user can use escape or clicking the backdrop to close the dialog */
disableClose?: boolean; disableClose?: boolean;
/**
* Custom accept action. Runs when the user clicks the accept button and shows a loading spinner until the promise
* is resolved.
*/
acceptAction?: () => Promise<void>;
}; };

View File

@ -20,7 +20,7 @@ export class BitActionDirective implements OnDestroy {
disabled = false; disabled = false;
@Input("bitAction") protected handler: FunctionReturningAwaitable; @Input("bitAction") handler: FunctionReturningAwaitable;
readonly loading$ = this._loading$.asObservable(); readonly loading$ = this._loading$.asObservable();

View File

@ -18,7 +18,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
private _loading$ = new BehaviorSubject<boolean>(false); private _loading$ = new BehaviorSubject<boolean>(false);
private _disabled$ = new BehaviorSubject<boolean>(false); private _disabled$ = new BehaviorSubject<boolean>(false);
@Input("bitSubmit") protected handler: FunctionReturningAwaitable; @Input("bitSubmit") handler: FunctionReturningAwaitable;
@Input() allowDisabledFormSubmit?: boolean = false; @Input() allowDisabledFormSubmit?: boolean = false;

View File

@ -1,8 +1,10 @@
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog"; import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { ReactiveFormsModule } from "@angular/forms";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { AsyncActionsModule } from "../async-actions";
import { ButtonModule } from "../button"; import { ButtonModule } from "../button";
import { IconButtonModule } from "../icon-button"; import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared"; import { SharedModule } from "../shared";
@ -15,7 +17,14 @@ import { SimpleConfigurableDialogComponent } from "./simple-configurable-dialog/
import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({ @NgModule({
imports: [SharedModule, IconButtonModule, CdkDialogModule, ButtonModule], imports: [
SharedModule,
AsyncActionsModule,
ButtonModule,
CdkDialogModule,
IconButtonModule,
ReactiveFormsModule,
],
declarations: [ declarations: [
DialogCloseDirective, DialogCloseDirective,
DialogTitleContainerDirective, DialogTitleContainerDirective,

View File

@ -1,28 +1,26 @@
<bit-simple-dialog> <form [formGroup]="formGroup" [bitSubmit]="accept">
<i bitDialogIcon class="bwi tw-text-3xl" [class]="iconClasses" aria-hidden="true"></i> <bit-simple-dialog>
<i bitDialogIcon class="bwi tw-text-3xl" [class]="iconClasses" aria-hidden="true"></i>
<span bitDialogTitle>{{ title }}</span> <span bitDialogTitle>{{ title }}</span>
<div bitDialogContent>{{ content }}</div> <div bitDialogContent>{{ content }}</div>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button <button type="submit" bitButton bitFormButton buttonType="primary">
type="button" {{ acceptButtonText }}
bitButton </button>
buttonType="primary"
(click)="dialogRef.close(SimpleDialogCloseType.ACCEPT)"
>
{{ acceptButtonText }}
</button>
<button <button
*ngIf="showCancelButton" *ngIf="showCancelButton"
type="button" type="button"
bitButton bitButton
buttonType="secondary" bitFormButton
(click)="dialogRef.close(SimpleDialogCloseType.CANCEL)" buttonType="secondary"
> (click)="dialogRef.close(SimpleDialogCloseType.CANCEL)"
{{ cancelButtonText }} >
</button> {{ cancelButtonText }}
</ng-container> </button>
</bit-simple-dialog> </ng-container>
</bit-simple-dialog>
</form>

View File

@ -1,5 +1,6 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { FormGroup } from "@angular/forms";
import { import {
SimpleDialogType, SimpleDialogType,
@ -29,8 +30,8 @@ const DEFAULT_COLOR: Record<SimpleDialogType, string> = {
templateUrl: "./simple-configurable-dialog.component.html", templateUrl: "./simple-configurable-dialog.component.html",
}) })
export class SimpleConfigurableDialogComponent { export class SimpleConfigurableDialogComponent {
SimpleDialogType = SimpleDialogType; protected SimpleDialogType = SimpleDialogType;
SimpleDialogCloseType = SimpleDialogCloseType; protected SimpleDialogCloseType = SimpleDialogCloseType;
get iconClasses() { get iconClasses() {
return [ return [
@ -39,12 +40,13 @@ export class SimpleConfigurableDialogComponent {
]; ];
} }
title: string; protected title: string;
content: string; protected content: string;
acceptButtonText: string; protected acceptButtonText: string;
cancelButtonText: string; protected cancelButtonText: string;
protected formGroup = new FormGroup({});
showCancelButton = this.simpleDialogOpts.cancelButtonText !== null; protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null;
constructor( constructor(
public dialogRef: DialogRef, public dialogRef: DialogRef,
@ -54,6 +56,14 @@ export class SimpleConfigurableDialogComponent {
this.localizeText(); this.localizeText();
} }
protected accept = async () => {
if (this.simpleDialogOpts.acceptAction) {
await this.simpleDialogOpts.acceptAction();
}
this.dialogRef.close(SimpleDialogCloseType.ACCEPT);
};
private localizeText() { private localizeText() {
this.title = this.translate(this.simpleDialogOpts.title); this.title = this.translate(this.simpleDialogOpts.title);
this.content = this.translate(this.simpleDialogOpts.content); this.content = this.translate(this.simpleDialogOpts.content);

View File

@ -15,96 +15,17 @@ import { DialogModule } from "../dialog.module";
@Component({ @Component({
template: ` template: `
<h2 class="tw-text-main">Dialog Type Examples:</h2> <div *ngFor="let group of dialogs">
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2"> <h2>{{ group.title }}</h2>
<button <div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
bitButton <button
buttonType="primary" *ngFor="let dialog of group.dialogs"
(click)="openSimpleConfigurableDialog(primaryLocalizedSimpleDialogOpts)" bitButton
> (click)="openSimpleConfigurableDialog(dialog)"
Open Primary Type Simple Dialog >
</button> {{ dialog.title }}
</button>
<button </div>
bitButton
buttonType="secondary"
(click)="openSimpleConfigurableDialog(successLocalizedSimpleDialogOpts)"
>
Open Success Type Simple Dialog
</button>
<button
bitButton
buttonType="secondary"
(click)="openSimpleConfigurableDialog(infoLocalizedSimpleDialogOpts)"
>
Open Info Type Simple Dialog
</button>
<button
bitButton
buttonType="secondary"
(click)="openSimpleConfigurableDialog(warningLocalizedSimpleDialogOpts)"
>
Open Warning Type Simple Dialog
</button>
<button
bitButton
buttonType="secondary"
(click)="openSimpleConfigurableDialog(dangerLocalizedSimpleDialogOpts)"
>
Open Danger Type Simple Dialog
</button>
</div>
<h2 class="tw-text-main">Custom Button Examples:</h2>
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
<button
bitButton
buttonType="primary"
(click)="openSimpleConfigurableDialog(primaryAcceptBtnOverrideSimpleDialogOpts)"
>
Open Simple Dialog with custom accept button text
</button>
<button
bitButton
buttonType="primary"
(click)="openSimpleConfigurableDialog(primaryCustomBtnsSimpleDialogOpts)"
>
Open Simple Dialog with 2 custom buttons
</button>
<button
bitButton
buttonType="primary"
(click)="openSimpleConfigurableDialog(primarySingleBtnSimpleDialogOpts)"
>
Open Single Button Simple Dialog
</button>
</div>
<h2 class="tw-text-main">Custom Icon Example:</h2>
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
<button
bitButton
buttonType="primary"
(click)="openSimpleConfigurableDialog(primaryCustomIconSimpleDialogOpts)"
>
Open Simple Dialog with custom icon
</button>
</div>
<h2 class="tw-text-main">Additional Examples:</h2>
<div class="tw-mb-4 tw-flex tw-flex-row tw-gap-2">
<button
bitButton
buttonType="primary"
(click)="openSimpleConfigurableDialog(primaryDisableCloseSimpleDialogOpts)"
>
Open Simple Dialog with backdrop click / escape key press disabled
</button>
</div> </div>
<bit-callout *ngIf="showCallout" [type]="calloutType" title="Dialog Close Result"> <bit-callout *ngIf="showCallout" [type]="calloutType" title="Dialog Close Result">
@ -113,72 +34,93 @@ import { DialogModule } from "../dialog.module";
`, `,
}) })
class StoryDialogComponent { class StoryDialogComponent {
primaryLocalizedSimpleDialogOpts: SimpleDialogOptions = { protected dialogs: { title: string; dialogs: SimpleDialogOptions[] }[] = [
title: this.i18nService.t("primaryTypeSimpleDialog"), {
content: this.i18nService.t("dialogContent"), title: "Regular",
type: SimpleDialogType.PRIMARY, dialogs: [
}; {
title: this.i18nService.t("primaryTypeSimpleDialog"),
successLocalizedSimpleDialogOpts: SimpleDialogOptions = { content: this.i18nService.t("dialogContent"),
title: this.i18nService.t("successTypeSimpleDialog"), type: SimpleDialogType.PRIMARY,
content: this.i18nService.t("dialogContent"), },
type: SimpleDialogType.SUCCESS, {
}; title: this.i18nService.t("successTypeSimpleDialog"),
content: this.i18nService.t("dialogContent"),
infoLocalizedSimpleDialogOpts: SimpleDialogOptions = { type: SimpleDialogType.SUCCESS,
title: this.i18nService.t("infoTypeSimpleDialog"), },
content: this.i18nService.t("dialogContent"), {
type: SimpleDialogType.INFO, title: this.i18nService.t("infoTypeSimpleDialog"),
}; content: this.i18nService.t("dialogContent"),
type: SimpleDialogType.INFO,
warningLocalizedSimpleDialogOpts: SimpleDialogOptions = { },
title: this.i18nService.t("warningTypeSimpleDialog"), {
content: this.i18nService.t("dialogContent"), title: this.i18nService.t("warningTypeSimpleDialog"),
type: SimpleDialogType.WARNING, content: this.i18nService.t("dialogContent"),
}; type: SimpleDialogType.WARNING,
},
dangerLocalizedSimpleDialogOpts: SimpleDialogOptions = { {
title: this.i18nService.t("dangerTypeSimpleDialog"), title: this.i18nService.t("dangerTypeSimpleDialog"),
content: this.i18nService.t("dialogContent"), content: this.i18nService.t("dialogContent"),
type: SimpleDialogType.DANGER, type: SimpleDialogType.DANGER,
}; },
],
primarySingleBtnSimpleDialogOpts: SimpleDialogOptions = { },
title: this.i18nService.t("primaryTypeSimpleDialog"), {
content: this.i18nService.t("dialogContent"), title: "Custom",
type: SimpleDialogType.PRIMARY, dialogs: [
acceptButtonText: "Ok", {
cancelButtonText: null, title: this.i18nService.t("primaryTypeSimpleDialog"),
}; content: this.i18nService.t("dialogContent"),
type: SimpleDialogType.PRIMARY,
primaryCustomBtnsSimpleDialogOpts: SimpleDialogOptions = { acceptButtonText: "Ok",
title: this.i18nService.t("primaryTypeSimpleDialog"), cancelButtonText: null,
content: this.i18nService.t("dialogContent"), },
type: SimpleDialogType.PRIMARY, {
acceptButtonText: this.i18nService.t("accept"), title: this.i18nService.t("primaryTypeSimpleDialog"),
cancelButtonText: this.i18nService.t("decline"), content: this.i18nService.t("dialogContent"),
}; type: SimpleDialogType.PRIMARY,
acceptButtonText: this.i18nService.t("accept"),
primaryAcceptBtnOverrideSimpleDialogOpts: SimpleDialogOptions = { cancelButtonText: this.i18nService.t("decline"),
title: this.i18nService.t("primaryTypeSimpleDialog"), },
content: this.i18nService.t("dialogContent"), {
type: SimpleDialogType.PRIMARY, title: this.i18nService.t("primaryTypeSimpleDialog"),
acceptButtonText: "Ok", content: this.i18nService.t("dialogContent"),
}; type: SimpleDialogType.PRIMARY,
acceptButtonText: "Ok",
primaryCustomIconSimpleDialogOpts: SimpleDialogOptions = { },
title: this.i18nService.t("primaryTypeSimpleDialog"), ],
content: this.i18nService.t("dialogContent"), },
type: SimpleDialogType.PRIMARY, {
icon: "bwi-family", title: "Icon",
}; dialogs: [
{
primaryDisableCloseSimpleDialogOpts: SimpleDialogOptions = { title: this.i18nService.t("primaryTypeSimpleDialog"),
title: this.i18nService.t("primaryTypeSimpleDialog"), content: this.i18nService.t("dialogContent"),
content: this.i18nService.t("dialogContent"), type: SimpleDialogType.PRIMARY,
type: SimpleDialogType.PRIMARY, icon: "bwi-family",
disableClose: true, },
}; ],
},
{
title: "Additional",
dialogs: [
{
title: this.i18nService.t("primaryTypeSimpleDialog"),
content: this.i18nService.t("dialogContent"),
type: SimpleDialogType.PRIMARY,
disableClose: true,
},
{
title: this.i18nService.t("asyncTypeSimpleDialog"),
content: this.i18nService.t("dialogContent"),
acceptAction: () => {
return new Promise((resolve) => setTimeout(resolve, 10000));
},
type: SimpleDialogType.PRIMARY,
},
],
},
];
showCallout = false; showCallout = false;
calloutType = "info"; calloutType = "info";
@ -216,6 +158,7 @@ export default {
infoTypeSimpleDialog: "Info Type Simple Dialog", infoTypeSimpleDialog: "Info Type Simple Dialog",
warningTypeSimpleDialog: "Warning Type Simple Dialog", warningTypeSimpleDialog: "Warning Type Simple Dialog",
dangerTypeSimpleDialog: "Danger Type Simple Dialog", dangerTypeSimpleDialog: "Danger Type Simple Dialog",
asyncTypeSimpleDialog: "Async",
dialogContent: "Dialog content goes here", dialogContent: "Dialog content goes here",
yes: "Yes", yes: "Yes",
no: "No", no: "No",