mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01: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:
parent
497b08df44
commit
d40a891f71
@ -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;
|
||||
}
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
<bit-simple-dialog>
|
||||
<i
|
||||
bit-dialog-icon
|
||||
class="bwi bwi-business tw-text-5xl tw-text-primary-500"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span bitDialogTitle class="font-bold">{{ "upgradeOrganization" | i18n }}</span>
|
||||
<span bitDialogContent>
|
||||
{{ data.dialogBodyText }}
|
||||
</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<ng-container *ngIf="data.orgCanManageBilling">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[routerLink]="['/organizations', data.orgId, 'billing', 'subscription']"
|
||||
[queryParams]="{ upgrade: true }"
|
||||
(click)="dialogRef.close()"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" (click)="dialogRef.close()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!data.orgCanManageBilling">
|
||||
<button bitButton buttonType="primary" (click)="dialogRef.close()">
|
||||
{{ "ok" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</bit-simple-dialog>
|
@ -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
|
||||
) {}
|
||||
}
|
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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,
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum SimpleDialogCloseType {
|
||||
ACCEPT = "accept",
|
||||
CANCEL = "cancel",
|
||||
}
|
@ -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;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export enum SimpleDialogType {
|
||||
PRIMARY = "primary",
|
||||
SUCCESS = "success",
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
DANGER = "danger",
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export interface Translation {
|
||||
key: string;
|
||||
placeholders?: Array<string | number>;
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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({});
|
Loading…
Reference in New Issue
Block a user