[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:
parent
2187db2153
commit
4b1570b0b3
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue