1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

Feature/SG-878 - Add open simple dialog method on dialog service (#4425)

* SG-878 - First draft - Dialog service now has backwards compatible method for opening a configurable simple dialog.

* SG-878 - People comp - test cases for simple dialog method - more testing required

* SG-878 - Much more simple dialog work - investigating different approaches to see what will work best. Lots of WIP on this one. Includes first draft but working solution for solving placeholder support for non-localized strings.

* SG-878 - (1) Broke out enums and types into separate files for better single responsibility (2) Allow null cancelButtonText for single accept button support

* SG-878 - Configurable simple dialog - removed separate comp approach as it is a maint problem to have simple dialog implemented in two places.

* SG-878 - Added js doc comments for dialog service openSimpleDialog method

* SG-878 - Don't export ConfigurableSimpleDialogComp as only dialogService should use it

* SG-878 - (1) Refactor configurable simple dialog to reduce icon class repetition in html (2) Update simple dialog options to use new Translation interface and update comp to properly process placeholders again

* SG-878 - Reverting all simple dialog changes as, per discussion with Oscar, going to use composition and go with configurable-simple-dialog comp for use in the dialog service

* SG-878 - Testing latest simple dialog changes

* SG-878 - Update simple-dialog-options

* SG-878 - (1) People & collections component now use dialogService.openSimpleDialog vs custom org upgrade dialog comp (2) Rename configurable-simple-dialog to simple-configurable-dialog for better folder placement

* SG-878 - Update formatting of Simple dialog options js doc comments

* SG-878 - Remove test code

* SG-878 -Remove org upgrade dialog component as it has been replaced with dialog service openSimpleDialog method call

* SG-878 - Move models to be near where they are used which is in the simple-configurable-dialog folder.

* SG-878 - Refactor icon classes into simple getter per Oscar's suggestions

* SG-878 - Refactor Translation placeholderValues to be just placeholders

* SG-878 - Refactor Simple Dialog Options to remove isLocalized as it doesn't buy us that much to have it. We can just check if a passed in value is a string or a Translation object to determine if we need to translate it.

* SG-878 - Dialog Svc - remove backdrop classes from openSimpleDialog method as standard open method applies them

* SG-878 - (1) Refactor simple configurable dialog to use comp properties instead of re-using option props (2) Reduce html complexity  (3) Create translate func for code simplification (4)  Remove isTranslation type guard as simple object check is sufficient

* SG-668 - Refactoring collections & people comps use of dialog service openSimpleDialog to condense options per PR feedback

* SG-878 - SimpleConfigDialog - (1) Footer classes were missing so btns were not spaced out properly (2) cancel btn text fixed to reference component property

* SG-878 - First pass at creating a storybook example for the new openSimpleDialog method on the dialogService.

* SG-878 - SimpleConfigurableDialog storybook - now displays callout with dialog results for better example

* SG-878 - SimpleConfigurableDialog - use text-main tailwind class for h2s so that text is colored properly on black background

* SG-878 - SimpleConfigurableDialog - Remove unstyled buttons and colored text and replace with plain secondary buttons to fix visibility issues on dark background.

* Update libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-type.enum.ts

SG-878 - Remove early commentary about use of simple-dialog-type enum

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.html

SG-878 - SimpleConfigurableDialog html - consolidate title html to 1 line

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts

SG-878 - SimpleConfigurableDialog comp ts - remove unnecessary comment.

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts

SG-878 - SimpleConfigDialog storybook fixes

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts

SG-878 - SimpleConfigDialog storybook fixes

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts

SG-878 - SimpleConfigDialog comp - remove unnecessary comment

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* SG-668 / SG-878 - Migrate Free Org Upgrade Flows logic from deprecated collections component to vault component

* SG-878 - Refactor the free org upgrade dialog to leverage separate methods to improve code and decrease cyclomatic complexity

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Jared Snider
2023-01-24 16:38:14 -05:00
committed by GitHub
parent 497b08df44
commit d40a891f71
16 changed files with 609 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
import { NgModule } from "@angular/core";
import { ButtonModule } from "../button";
import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared";
@@ -8,15 +9,17 @@ import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleConfigurableDialogComponent } from "./simple-configurable-dialog/simple-configurable-dialog.component";
import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({
imports: [SharedModule, IconButtonModule, CdkDialogModule],
imports: [SharedModule, IconButtonModule, CdkDialogModule, ButtonModule],
declarations: [
DialogCloseDirective,
DialogTitleContainerDirective,
DialogComponent,
SimpleDialogComponent,
SimpleConfigurableDialogComponent,
IconDirective,
],
exports: [

View File

@@ -21,21 +21,20 @@ import { filter, Subject, switchMap, takeUntil } from "rxjs";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { SimpleDialogOptions } from "./simple-configurable-dialog/models/simple-dialog-options";
import { SimpleConfigurableDialogComponent } from "./simple-configurable-dialog/simple-configurable-dialog.component";
@Injectable()
export class DialogService extends Dialog implements OnDestroy {
private _destroy$ = new Subject<void>();
override open<R = unknown, D = unknown, C = unknown>(
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
config?: DialogConfig<D, DialogRef<R, C>>
): DialogRef<R, C> {
config = {
backdropClass: ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0", "tw-z-40"],
...config,
};
return super.open(componentOrTemplateRef, config);
}
private backDropClasses = [
"tw-fixed",
"tw-bg-black",
"tw-bg-opacity-30",
"tw-inset-0",
"tw-z-40",
];
constructor(
/** Parent class constructor */
@@ -70,4 +69,32 @@ export class DialogService extends Dialog implements OnDestroy {
this._destroy$.complete();
super.ngOnDestroy();
}
override open<R = unknown, D = unknown, C = unknown>(
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
config?: DialogConfig<D, DialogRef<R, C>>
): DialogRef<R, C> {
config = {
backdropClass: this.backDropClasses,
...config,
};
return super.open(componentOrTemplateRef, config);
}
/**
* Opens a simple dialog.
*
* @param {SimpleDialogOptions} simpleDialogOptions - An object containing options for the dialog.
* @returns `DialogRef` - The reference to the opened dialog.
* Contains a closed observable which can be subscribed to for determining which button
* a user pressed (see `SimpleDialogCloseType`)
*/
openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): DialogRef {
// Method needs to return dialog reference so devs can sub to closed and get results.
return this.open(SimpleConfigurableDialogComponent, {
data: simpleDialogOptions,
disableClose: simpleDialogOptions.disableClose,
});
}
}

View File

@@ -1,2 +1,5 @@
export * from "./dialog.module";
export * from "./dialog.service";
export * from "./simple-configurable-dialog/models/simple-dialog-options";
export * from "./simple-configurable-dialog/models/simple-dialog-type.enum";
export * from "./simple-configurable-dialog/models/simple-dialog-close-type.enum";

View File

@@ -0,0 +1,4 @@
export enum SimpleDialogCloseType {
ACCEPT = "accept",
CANCEL = "cancel",
}

View File

@@ -0,0 +1,51 @@
import { SimpleDialogType } from "./simple-dialog-type.enum";
import { Translation } from "./translation";
// Using type lets devs skip optional params w/out having to pass undefined.
/**
*
* @typedef {Object} SimpleDialogOptions - A configuration type for the Simple Dialog component
*/
export type SimpleDialogOptions = {
/**
* Dialog title.
*
* If not localized, pass in a `Translation`. */
title: string | Translation;
/** Dialog content.
*
* If not localized, pass in a `Translation`. */
content: string | Translation;
/** Dialog type. It controls default icons and icon colors. */
type: SimpleDialogType;
/** Dialog custom icon class.
*
* If not provided, a standard icon will be inferred from type.
* Note: icon color is enforced based on dialog type. */
icon?: string;
/** Dialog custom accept button text.
*
* If not provided, ("yes" | i18n) will be used.
*
* If not localized, pass in a `Translation` */
acceptButtonText?: string | Translation;
/**
* Dialog custom cancel button text.
*
* If not provided, ("no" | i18n) will be used.
*
* If custom acceptButtonText is passed in, ("cancel" | i18n) will be used.
*
* If null is provided, the cancel button will be removed.
*
* If not localized, pass in a `Translation` */
cancelButtonText?: string | Translation;
/** Whether or not the user can use escape or clicking the backdrop to close the dialog */
disableClose?: boolean;
};

View File

@@ -0,0 +1,7 @@
export enum SimpleDialogType {
PRIMARY = "primary",
SUCCESS = "success",
INFO = "info",
WARNING = "warning",
DANGER = "danger",
}

View File

@@ -0,0 +1,4 @@
export interface Translation {
key: string;
placeholders?: Array<string | number>;
}

View File

@@ -0,0 +1,22 @@
<bit-simple-dialog>
<i bit-dialog-icon class="bwi tw-text-3xl" [class]="iconClasses" aria-hidden="true"></i>
<span bitDialogTitle>{{ title }}</span>
<div bitDialogContent>{{ content }}</div>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary" (click)="dialogRef.close(SimpleDialogCloseType.ACCEPT)">
{{ acceptButtonText }}
</button>
<button
*ngIf="showCancelButton"
bitButton
buttonType="secondary"
(click)="dialogRef.close(SimpleDialogCloseType.CANCEL)"
>
{{ cancelButtonText }}
</button>
</div>
</bit-simple-dialog>

View File

@@ -0,0 +1,80 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { SimpleDialogCloseType } from "./models/simple-dialog-close-type.enum";
import { SimpleDialogOptions } from "./models/simple-dialog-options";
import { SimpleDialogType } from "./models/simple-dialog-type.enum";
import { Translation } from "./models/translation";
const DEFAULT_ICON: Record<SimpleDialogType, string> = {
[SimpleDialogType.PRIMARY]: "bwi-business",
[SimpleDialogType.SUCCESS]: "bwi-star",
[SimpleDialogType.INFO]: "bwi-info-circle",
[SimpleDialogType.WARNING]: "bwi-exclamation-triangle",
[SimpleDialogType.DANGER]: "bwi-error",
};
const DEFAULT_COLOR: Record<SimpleDialogType, string> = {
[SimpleDialogType.PRIMARY]: "tw-text-primary-500",
[SimpleDialogType.SUCCESS]: "tw-text-success",
[SimpleDialogType.INFO]: "tw-text-info",
[SimpleDialogType.WARNING]: "tw-text-warning",
[SimpleDialogType.DANGER]: "tw-text-danger",
};
@Component({
selector: "bit-simple-configurable-dialog",
templateUrl: "./simple-configurable-dialog.component.html",
})
export class SimpleConfigurableDialogComponent {
SimpleDialogType = SimpleDialogType;
SimpleDialogCloseType = SimpleDialogCloseType;
get iconClasses() {
return [
this.simpleDialogOpts.icon ?? DEFAULT_ICON[this.simpleDialogOpts.type],
DEFAULT_COLOR[this.simpleDialogOpts.type],
];
}
title: string;
content: string;
acceptButtonText: string;
cancelButtonText: string;
showCancelButton = this.simpleDialogOpts.cancelButtonText !== null;
constructor(
public dialogRef: DialogRef,
private i18nService: I18nService,
@Inject(DIALOG_DATA) public simpleDialogOpts?: SimpleDialogOptions
) {
this.localizeText();
}
private localizeText() {
this.title = this.translate(this.simpleDialogOpts.title);
this.content = this.translate(this.simpleDialogOpts.content);
this.acceptButtonText = this.translate(this.simpleDialogOpts.acceptButtonText, "yes");
if (this.showCancelButton) {
// If accept text is overridden, use cancel, otherwise no
this.cancelButtonText = this.translate(
this.simpleDialogOpts.cancelButtonText,
this.simpleDialogOpts.acceptButtonText !== undefined ? "cancel" : "no"
);
}
}
private translate(translation: string | Translation, defaultKey?: string): string {
// Translation interface use implies we must localize.
if (typeof translation === "object") {
return this.i18nService.t(translation.key, ...translation.placeholders);
}
// Use string that is already translated or use default key post translate
return translation ?? this.i18nService.t(defaultKey);
}
}

View File

@@ -0,0 +1,255 @@
import { DialogModule, DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../../button";
import { CalloutModule } from "../../callout";
import { IconButtonModule } from "../../icon-button";
import { SharedModule } from "../../shared/shared.module";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogService } from "../dialog.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "../simple-dialog/simple-dialog.component";
import { SimpleDialogCloseType } from "./models/simple-dialog-close-type.enum";
import { SimpleDialogOptions } from "./models/simple-dialog-options";
import { SimpleDialogType } from "./models/simple-dialog-type.enum";
@Component({
selector: "app-story-dialog",
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>
<bit-callout *ngIf="showCallout" [type]="calloutType" title="Dialog Close Result">
<span *ngIf="dialogCloseResult">{{ dialogCloseResult }}</span>
<span *ngIf="!dialogCloseResult">undefined</span>
</bit-callout>
`,
})
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,
};
showCallout = false;
calloutType = "info";
dialogCloseResult: undefined | SimpleDialogCloseType;
constructor(public dialogService: DialogService, private i18nService: I18nService) {}
openSimpleConfigurableDialog(opts: SimpleDialogOptions) {
const dialogReference: DialogRef = this.dialogService.openSimpleDialog(opts);
firstValueFrom(dialogReference.closed).then((result: SimpleDialogCloseType | undefined) => {
this.showCallout = true;
this.dialogCloseResult = result;
if (result && result === SimpleDialogCloseType.ACCEPT) {
this.calloutType = "success";
} else {
this.calloutType = "info";
}
});
}
}
export default {
title: "Component Library/Dialogs/Service/SimpleConfigurable",
component: StoryDialogComponent,
decorators: [
moduleMetadata({
declarations: [DialogCloseDirective, DialogTitleContainerDirective, SimpleDialogComponent],
imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule, CalloutModule],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
primaryTypeSimpleDialog: "Primary Type Simple Dialog",
successTypeSimpleDialog: "Success Type Simple Dialog",
infoTypeSimpleDialog: "Info Type Simple Dialog",
warningTypeSimpleDialog: "Warning Type Simple Dialog",
dangerTypeSimpleDialog: "Danger Type Simple Dialog",
dialogContent: "Dialog content goes here",
yes: "Yes",
no: "No",
ok: "Ok",
cancel: "Cancel",
accept: "Accept",
decline: "Decline",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library",
},
},
} as Meta;
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
props: args,
});
export const Default = Template.bind({});