diff --git a/apps/web/src/app/organizations/manage/collections.component.ts b/apps/web/src/app/organizations/manage/collections.component.ts index a70ff2b12e..94407cc64b 100644 --- a/apps/web/src/app/organizations/manage/collections.component.ts +++ b/apps/web/src/app/organizations/manage/collections.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, lastValueFrom } from "rxjs"; import { first } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -21,12 +21,16 @@ import { } from "@bitwarden/common/models/response/collection.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CollectionView } from "@bitwarden/common/models/view/collection.view"; -import { DialogService } from "@bitwarden/components"; +import { + DialogService, + SimpleDialogCloseType, + SimpleDialogOptions, + SimpleDialogType, +} from "@bitwarden/components"; import { CollectionDialogResult, openCollectionDialog } from "../shared"; import { EntityUsersComponent } from "./entity-users.component"; -import { OrgUpgradeDialogComponent } from "./org-upgrade-dialog/org-upgrade-dialog.component"; @Component({ selector: "app-org-manage-collections", @@ -62,7 +66,8 @@ export class CollectionsComponent implements OnInit { private searchService: SearchService, private logService: LogService, private organizationService: OrganizationService, - private dialogService: DialogService + private dialogService: DialogService, + private router: Router ) {} async ngOnInit() { @@ -138,23 +143,42 @@ export class CollectionsComponent implements OnInit { this.collections.length === this.organization.maxCollections ) { // Show org upgrade modal - const dialogBodyText = this.organization.canManageBilling - ? this.i18nService.t( - "freeOrgMaxCollectionReachedManageBilling", - this.organization.maxCollections.toString() - ) - : this.i18nService.t( - "freeOrgMaxCollectionReachedNoManageBilling", - this.organization.maxCollections.toString() - ); + // It might be worth creating a simple + // org upgrade dialog service to launch the dialog here and in the people.comp + // once the enterprise pod is done w/ their organization module refactor. + const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { + title: this.i18nService.t("upgradeOrganization"), + content: this.i18nService.t( + this.organization.canManageBilling + ? "freeOrgMaxCollectionReachedManageBilling" + : "freeOrgMaxCollectionReachedNoManageBilling", + this.organization.maxCollections + ), + type: SimpleDialogType.PRIMARY, + }; - this.dialogService.open(OrgUpgradeDialogComponent, { - data: { - orgId: this.organization.id, - dialogBodyText: dialogBodyText, - orgCanManageBilling: this.organization.canManageBilling, - }, + if (this.organization.canManageBilling) { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); + } else { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); + orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn + } + + const simpleDialog = this.dialogService.openSimpleDialog(orgUpgradeSimpleDialogOpts); + + firstValueFrom(simpleDialog.closed).then((result: SimpleDialogCloseType | undefined) => { + if (!result) { + return; + } + + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + this.router.navigate( + ["/organizations", this.organization.id, "billing", "subscription"], + { queryParams: { upgrade: true } } + ); + } }); + return; } diff --git a/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.html b/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.html deleted file mode 100644 index 16e99d76ff..0000000000 --- a/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - {{ "upgradeOrganization" | i18n }} - - {{ data.dialogBodyText }} - -
- - - - - - - - -
-
diff --git a/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.ts b/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.ts deleted file mode 100644 index 9ebdef9be1..0000000000 --- a/apps/web/src/app/organizations/manage/org-upgrade-dialog/org-upgrade-dialog.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; - -export interface OrgUpgradeDialogData { - orgId: string; - orgCanManageBilling: boolean; - dialogBodyText: string; -} - -@Component({ - selector: "app-org-upgrade-dialog", - templateUrl: "org-upgrade-dialog.component.html", -}) -export class OrgUpgradeDialogComponent { - constructor( - public dialogRef: DialogRef, - @Inject(DIALOG_DATA) public data: OrgUpgradeDialogData - ) {} -} diff --git a/apps/web/src/app/organizations/members/people.component.ts b/apps/web/src/app/organizations/members/people.component.ts index f8be944432..5918538e4c 100644 --- a/apps/web/src/app/organizations/members/people.component.ts +++ b/apps/web/src/app/organizations/members/people.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { combineLatest, concatMap, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -34,13 +34,17 @@ import { Organization } from "@bitwarden/common/models/domain/organization"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request"; import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { DialogService } from "@bitwarden/components"; +import { + DialogService, + SimpleDialogCloseType, + SimpleDialogOptions, + SimpleDialogType, +} from "@bitwarden/components"; import { BasePeopleComponent } from "../../common/base.people.component"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; import { EntityEventsComponent } from "../manage/entity-events.component"; -import { OrgUpgradeDialogComponent } from "../manage/org-upgrade-dialog/org-upgrade-dialog.component"; import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component"; @@ -105,6 +109,7 @@ export class PeopleComponent private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, private dialogService: DialogService, + private router: Router, private groupService: GroupService, private collectionService: CollectionService ) { @@ -306,6 +311,40 @@ export class PeopleComponent ); } + private async showFreeOrgUpgradeDialog(): Promise { + const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { + title: this.i18nService.t("upgradeOrganization"), + content: this.i18nService.t( + this.organization.canManageBilling + ? "freeOrgInvLimitReachedManageBilling" + : "freeOrgInvLimitReachedNoManageBilling", + this.organization.seats + ), + type: SimpleDialogType.PRIMARY, + }; + + if (this.organization.canManageBilling) { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); + } else { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); + orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn + } + + const simpleDialog = this.dialogService.openSimpleDialog(orgUpgradeSimpleDialogOpts); + + firstValueFrom(simpleDialog.closed).then((result: SimpleDialogCloseType | undefined) => { + if (!result) { + return; + } + + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { + queryParams: { upgrade: true }, + }); + } + }); + } + async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) { // Invite User: Add Flow // Click on user email: Edit Flow @@ -317,24 +356,7 @@ export class PeopleComponent this.allUsers.length === this.organization.seats ) { // Show org upgrade modal - - const dialogBodyText = this.organization.canManageBilling - ? this.i18nService.t( - "freeOrgInvLimitReachedManageBilling", - this.organization.seats.toString() - ) - : this.i18nService.t( - "freeOrgInvLimitReachedNoManageBilling", - this.organization.seats.toString() - ); - - this.dialogService.open(OrgUpgradeDialogComponent, { - data: { - orgId: this.organization.id, - orgCanManageBilling: this.organization.canManageBilling, - dialogBodyText: dialogBodyText, - }, - }); + await this.showFreeOrgUpgradeDialog(); return; } diff --git a/apps/web/src/app/organizations/organization.module.ts b/apps/web/src/app/organizations/organization.module.ts index cdfffc240e..32b1df1660 100644 --- a/apps/web/src/app/organizations/organization.module.ts +++ b/apps/web/src/app/organizations/organization.module.ts @@ -3,7 +3,6 @@ import { NgModule } from "@angular/core"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupsComponent } from "./manage/groups.component"; -import { OrgUpgradeDialogComponent } from "./manage/org-upgrade-dialog/org-upgrade-dialog.component"; import { OrganizationsRoutingModule } from "./organization-routing.module"; import { SharedOrganizationModule } from "./shared"; import { AccessSelectorModule } from "./shared/components/access-selector"; @@ -15,6 +14,6 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; CoreOrganizationModule, OrganizationsRoutingModule, ], - declarations: [GroupsComponent, GroupAddEditComponent, OrgUpgradeDialogComponent], + declarations: [GroupsComponent, GroupAddEditComponent], }) export class OrganizationModule {} diff --git a/apps/web/src/app/organizations/vault/vault.component.ts b/apps/web/src/app/organizations/vault/vault.component.ts index 4d4f50959f..86c2cecc78 100644 --- a/apps/web/src/app/organizations/vault/vault.component.ts +++ b/apps/web/src/app/organizations/vault/vault.component.ts @@ -20,14 +20,21 @@ import { OrganizationService } from "@bitwarden/common/abstractions/organization import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; +import { ProductType } from "@bitwarden/common/enums/productType"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; +import { + DialogService, + SimpleDialogCloseType, + SimpleDialogOptions, + SimpleDialogType, +} from "@bitwarden/components"; import { VaultFilterService } from "../../vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model"; import { CollectionFilter } from "../../vault/vault-filter/shared/models/vault-filter.type"; +import { CollectionAdminService } from "../core"; import { EntityEventsComponent } from "../manage/entity-events.component"; import { CollectionDialogResult, @@ -79,7 +86,8 @@ export class VaultComponent implements OnInit, OnDestroy { private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService + private passwordRepromptService: PasswordRepromptService, + private collectionAdminService: CollectionAdminService ) {} async ngOnInit() { @@ -168,7 +176,49 @@ export class VaultComponent implements OnInit, OnDestroy { this.vaultItemsComponent.search(200); } + private showFreeOrgUpgradeDialog(): void { + const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { + title: this.i18nService.t("upgradeOrganization"), + content: this.i18nService.t( + this.organization.canManageBilling + ? "freeOrgMaxCollectionReachedManageBilling" + : "freeOrgMaxCollectionReachedNoManageBilling", + this.organization.maxCollections + ), + type: SimpleDialogType.PRIMARY, + }; + + if (this.organization.canManageBilling) { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); + } else { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); + orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn + } + + const simpleDialog = this.dialogService.openSimpleDialog(orgUpgradeSimpleDialogOpts); + + firstValueFrom(simpleDialog.closed).then((result: SimpleDialogCloseType | undefined) => { + if (!result) { + return; + } + + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { + queryParams: { upgrade: true }, + }); + } + }); + } + async addCollection() { + if (this.organization.planProductType === ProductType.Free) { + const collections = await this.collectionAdminService.getAll(this.organization.id); + if (collections.length === this.organization.maxCollections) { + this.showFreeOrgUpgradeDialog(); + return; + } + } + const dialog = openCollectionDialog(this.dialogService, { data: { organizationId: this.organization?.id, diff --git a/libs/components/src/dialog/dialog.module.ts b/libs/components/src/dialog/dialog.module.ts index 3932c0c85e..d5d1038b4b 100644 --- a/libs/components/src/dialog/dialog.module.ts +++ b/libs/components/src/dialog/dialog.module.ts @@ -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: [ diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index f23b3fd797..04e11f6c29 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -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(); - override open( - componentOrTemplateRef: ComponentType | TemplateRef, - config?: DialogConfig> - ): DialogRef { - 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( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: DialogConfig> + ): DialogRef { + 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, + }); + } } diff --git a/libs/components/src/dialog/index.ts b/libs/components/src/dialog/index.ts index 67872beb1f..7ee586158f 100644 --- a/libs/components/src/dialog/index.ts +++ b/libs/components/src/dialog/index.ts @@ -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"; diff --git a/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-close-type.enum.ts b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-close-type.enum.ts new file mode 100644 index 0000000000..d6eff80cdd --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-close-type.enum.ts @@ -0,0 +1,4 @@ +export enum SimpleDialogCloseType { + ACCEPT = "accept", + CANCEL = "cancel", +} diff --git a/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-options.ts b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-options.ts new file mode 100644 index 0000000000..c56e5b4703 --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-options.ts @@ -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; +}; diff --git a/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-type.enum.ts b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-type.enum.ts new file mode 100644 index 0000000000..e7fa460aac --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/models/simple-dialog-type.enum.ts @@ -0,0 +1,7 @@ +export enum SimpleDialogType { + PRIMARY = "primary", + SUCCESS = "success", + INFO = "info", + WARNING = "warning", + DANGER = "danger", +} diff --git a/libs/components/src/dialog/simple-configurable-dialog/models/translation.ts b/libs/components/src/dialog/simple-configurable-dialog/models/translation.ts new file mode 100644 index 0000000000..d8dca21b3f --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/models/translation.ts @@ -0,0 +1,4 @@ +export interface Translation { + key: string; + placeholders?: Array; +} diff --git a/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.html b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.html new file mode 100644 index 0000000000..3fcb0c58a3 --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.html @@ -0,0 +1,22 @@ + + + + {{ title }} + +
{{ content }}
+ +
+ + + +
+
diff --git a/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts new file mode 100644 index 0000000000..20f95fece8 --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -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.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.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); + } +} diff --git a/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts new file mode 100644 index 0000000000..fb0a47ce80 --- /dev/null +++ b/libs/components/src/dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -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: ` +

Dialog Type Examples:

+
+ + + + + + + + + +
+ +

Custom Button Examples:

+
+ + + + + +
+ +

Custom Icon Example:

+
+ +
+ +

Additional Examples:

+
+ +
+ + + {{ dialogCloseResult }} + undefined + + `, +}) +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 = (args: StoryDialogComponent) => ({ + props: args, +}); + +export const Default = Template.bind({});