[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 */
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;
@Input("bitAction") protected handler: FunctionReturningAwaitable;
@Input("bitAction") handler: FunctionReturningAwaitable;
readonly loading$ = this._loading$.asObservable();

View File

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

View File

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

View File

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

View File

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

View File

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