1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

Web - SG-668 update flows for free 2 person orgs (#4093)

* People-component - Minor refactoring - Make  org a comp prop instead of creating multiple component props for props on the org object

* Added IconDirective to Dialog.module so that bit-dialog-icon directive can work within <bit-simple-dialog> components

* SG-668 - #2 - If a free org has members (any status) at max seat limit, then prompt for upgrade with dialog which takes you to upgrade flow on billing/subscription management page

* SG-668 - (1) Refactored upgrade dialog to accept translated body text for better re-usability (2) Completed task #3 - If user has max collections for free org and tries to add a 3rd, they are prompted via upgrade dialog.

* SG-668 - Update equality checks to use strict equality

* SG-668 - Upgrade dialog now shows contextual body text based on if the user can manage billing or not
This commit is contained in:
Jared Snider 2022-12-16 15:11:37 -05:00 committed by GitHub
parent 9bbe13fa71
commit 3d008da287
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 47 deletions

View File

@ -75,6 +75,10 @@ export class OrganizationSubscriptionComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
if (this.route.snapshot.queryParamMap.get("upgrade")) {
this.changePlan();
}
this.route.params this.route.params
.pipe( .pipe(
concatMap(async (params) => { concatMap(async (params) => {

View File

@ -10,6 +10,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { ProductType } from "@bitwarden/common/enums/productType";
import { CollectionData } from "@bitwarden/common/models/data/collection.data"; import { CollectionData } from "@bitwarden/common/models/data/collection.data";
import { Collection } from "@bitwarden/common/models/domain/collection"; import { Collection } from "@bitwarden/common/models/domain/collection";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
@ -19,9 +20,11 @@ import {
} from "@bitwarden/common/models/response/collection.response"; } from "@bitwarden/common/models/response/collection.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { CollectionView } from "@bitwarden/common/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { DialogService } from "@bitwarden/components";
import { CollectionAddEditComponent } from "./collection-add-edit.component"; import { CollectionAddEditComponent } from "./collection-add-edit.component";
import { EntityUsersComponent } from "./entity-users.component"; import { EntityUsersComponent } from "./entity-users.component";
import { OrgUpgradeDialogComponent } from "./org-upgrade-dialog/org-upgrade-dialog.component";
@Component({ @Component({
selector: "app-org-manage-collections", selector: "app-org-manage-collections",
@ -56,7 +59,8 @@ export class CollectionsComponent implements OnInit {
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private searchService: SearchService, private searchService: SearchService,
private logService: LogService, private logService: LogService,
private organizationService: OrganizationService private organizationService: OrganizationService,
private dialogService: DialogService
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -126,6 +130,32 @@ export class CollectionsComponent implements OnInit {
return; return;
} }
if (
!collection &&
this.organization.planProductType === ProductType.Free &&
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()
);
this.dialogService.open(OrgUpgradeDialogComponent, {
data: {
orgId: this.organization.id,
dialogBodyText: dialogBodyText,
orgCanManageBilling: this.organization.canManageBilling,
},
});
return;
}
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(
CollectionAddEditComponent, CollectionAddEditComponent,
this.addEditModalRef, this.addEditModalRef,

View File

@ -0,0 +1,33 @@
<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>

View File

@ -0,0 +1,19 @@
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
) {}
}

View File

@ -221,7 +221,7 @@
href="#" href="#"
appStopClick appStopClick
(click)="groups(u)" (click)="groups(u)"
*ngIf="accessGroups" *ngIf="organization.useGroups"
> >
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
{{ "groups" | i18n }} {{ "groups" | i18n }}
@ -231,7 +231,7 @@
href="#" href="#"
appStopClick appStopClick
(click)="events(u)" (click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed" *ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
> >
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }} {{ "eventLogs" | i18n }}

View File

@ -20,12 +20,15 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
import { ProductType } from "@bitwarden/common/enums/productType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request";
import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request"; import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request"; import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response"; import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
import { DialogService } from "@bitwarden/components";
import { BasePeopleComponent } from "../../common/base.people.component"; import { BasePeopleComponent } from "../../common/base.people.component";
@ -34,6 +37,7 @@ import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component"; import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./bulk/bulk-status.component"; import { BulkStatusComponent } from "./bulk/bulk-status.component";
import { EntityEventsComponent } from "./entity-events.component"; import { EntityEventsComponent } from "./entity-events.component";
import { OrgUpgradeDialogComponent } from "./org-upgrade-dialog/org-upgrade-dialog.component";
import { ResetPasswordComponent } from "./reset-password.component"; import { ResetPasswordComponent } from "./reset-password.component";
import { UserAddEditComponent } from "./user-add-edit.component"; import { UserAddEditComponent } from "./user-add-edit.component";
import { UserGroupsComponent } from "./user-groups.component"; import { UserGroupsComponent } from "./user-groups.component";
@ -65,15 +69,9 @@ export class PeopleComponent
userType = OrganizationUserType; userType = OrganizationUserType;
userStatusType = OrganizationUserStatusType; userStatusType = OrganizationUserStatusType;
organizationId: string; organization: Organization;
status: OrganizationUserStatusType = null; status: OrganizationUserStatusType = null;
accessEvents = false;
accessGroups = false;
canResetPassword = false; // User permission (admin/custom)
orgUseResetPassword = false; // Org plan ability
orgHasKeys = false; // Org public/private keys
orgResetPasswordPolicyEnabled = false; orgResetPasswordPolicyEnabled = false;
callingUserType: OrganizationUserType = null;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -93,7 +91,8 @@ export class PeopleComponent
private syncService: SyncService, private syncService: SyncService,
stateService: StateService, stateService: StateService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService
) { ) {
super( super(
apiService, apiService,
@ -114,26 +113,23 @@ export class PeopleComponent
combineLatest([this.route.params, this.route.queryParams, this.policyService.policies$]) combineLatest([this.route.params, this.route.queryParams, this.policyService.policies$])
.pipe( .pipe(
concatMap(async ([params, qParams, policies]) => { concatMap(async ([params, qParams, policies]) => {
this.organizationId = params.organizationId; this.organization = await this.organizationService.get(params.organizationId);
const organization = await this.organizationService.get(this.organizationId);
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
this.canResetPassword = organization.canManageUsersPassword;
this.orgUseResetPassword = organization.useResetPassword;
this.callingUserType = organization.type;
this.orgHasKeys = organization.hasPublicAndPrivateKeys;
// Backfill pub/priv key if necessary // Backfill pub/priv key if necessary
if (this.canResetPassword && !this.orgHasKeys) { if (
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); this.organization.canManageUsersPassword &&
!this.organization.hasPublicAndPrivateKeys
) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organization.id);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
const response = await this.organizationApiService.updateKeys( const response = await this.organizationApiService.updateKeys(
this.organizationId, this.organization.id,
request request
); );
if (response != null) { if (response != null) {
this.orgHasKeys = response.publicKey != null && response.privateKey != null; this.organization.hasPublicAndPrivateKeys =
response.publicKey != null && response.privateKey != null;
await this.syncService.fullSync(true); // Replace oganizations with new data await this.syncService.fullSync(true); // Replace oganizations with new data
} else { } else {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
@ -142,7 +138,7 @@ export class PeopleComponent
const resetPasswordPolicy = policies const resetPasswordPolicy = policies
.filter((policy) => policy.type === PolicyType.ResetPassword) .filter((policy) => policy.type === PolicyType.ResetPassword)
.find((p) => p.organizationId === this.organizationId); .find((p) => p.organizationId === this.organization.id);
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
await this.load(); await this.load();
@ -171,41 +167,41 @@ export class PeopleComponent
} }
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> { getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
return this.apiService.getOrganizationUsers(this.organizationId); return this.apiService.getOrganizationUsers(this.organization.id);
} }
deleteUser(id: string): Promise<void> { deleteUser(id: string): Promise<void> {
return this.apiService.deleteOrganizationUser(this.organizationId, id); return this.apiService.deleteOrganizationUser(this.organization.id, id);
} }
revokeUser(id: string): Promise<void> { revokeUser(id: string): Promise<void> {
return this.apiService.revokeOrganizationUser(this.organizationId, id); return this.apiService.revokeOrganizationUser(this.organization.id, id);
} }
restoreUser(id: string): Promise<void> { restoreUser(id: string): Promise<void> {
return this.apiService.restoreOrganizationUser(this.organizationId, id); return this.apiService.restoreOrganizationUser(this.organization.id, id);
} }
reinviteUser(id: string): Promise<void> { reinviteUser(id: string): Promise<void> {
return this.apiService.postOrganizationUserReinvite(this.organizationId, id); return this.apiService.postOrganizationUserReinvite(this.organization.id, id);
} }
async confirmUser( async confirmUser(
user: OrganizationUserUserDetailsResponse, user: OrganizationUserUserDetailsResponse,
publicKey: Uint8Array publicKey: Uint8Array
): Promise<void> { ): Promise<void> {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId); const orgKey = await this.cryptoService.getOrgKey(this.organization.id);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest(); const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString; request.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request); await this.apiService.postOrganizationUserConfirm(this.organization.id, user.id, request);
} }
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean { allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
// Hierarchy check // Hierarchy check
let callingUserHasPermission = false; let callingUserHasPermission = false;
switch (this.callingUserType) { switch (this.organization.type) {
case OrganizationUserType.Owner: case OrganizationUserType.Owner:
callingUserHasPermission = true; callingUserHasPermission = true;
break; break;
@ -221,10 +217,10 @@ export class PeopleComponent
// Final // Final
return ( return (
this.canResetPassword && this.organization.canManageUsersPassword &&
callingUserHasPermission && callingUserHasPermission &&
this.orgUseResetPassword && this.organization.useResetPassword &&
this.orgHasKeys && this.organization.hasPublicAndPrivateKeys &&
orgUser.resetPasswordEnrolled && orgUser.resetPasswordEnrolled &&
this.orgResetPasswordPolicyEnabled && this.orgResetPasswordPolicyEnabled &&
orgUser.status === OrganizationUserStatusType.Confirmed orgUser.status === OrganizationUserStatusType.Confirmed
@ -233,20 +229,51 @@ export class PeopleComponent
showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean { showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean {
return ( return (
this.orgUseResetPassword && this.organization.useResetPassword &&
orgUser.resetPasswordEnrolled && orgUser.resetPasswordEnrolled &&
this.orgResetPasswordPolicyEnabled this.orgResetPasswordPolicyEnabled
); );
} }
async edit(user: OrganizationUserUserDetailsResponse) { async edit(user: OrganizationUserUserDetailsResponse) {
// Invite User: Add Flow
// Click on user email: Edit Flow
// User attempting to invite new users in a free org with max users
if (
!user &&
this.organization.planProductType === ProductType.Free &&
this.users.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,
},
});
return;
}
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(
UserAddEditComponent, UserAddEditComponent,
this.addEditModalRef, this.addEditModalRef,
(comp) => { (comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.organizationUserId = user != null ? user.id : null; comp.organizationUserId = user?.id || null;
comp.usesKeyConnector = user?.usesKeyConnector; comp.usesKeyConnector = user?.usesKeyConnector;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSavedUser.subscribe(() => { comp.onSavedUser.subscribe(() => {
@ -278,7 +305,7 @@ export class PeopleComponent
this.groupsModalRef, this.groupsModalRef,
(comp) => { (comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.organizationUserId = user != null ? user.id : null; comp.organizationUserId = user != null ? user.id : null;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSavedUser.subscribe(() => { comp.onSavedUser.subscribe(() => {
@ -297,7 +324,7 @@ export class PeopleComponent
BulkRemoveComponent, BulkRemoveComponent,
this.bulkRemoveModalRef, this.bulkRemoveModalRef,
(comp) => { (comp) => {
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.users = this.getCheckedUsers(); comp.users = this.getCheckedUsers();
} }
); );
@ -322,7 +349,7 @@ export class PeopleComponent
const ref = this.modalService.open(BulkRestoreRevokeComponent, { const ref = this.modalService.open(BulkRestoreRevokeComponent, {
allowMultipleModals: true, allowMultipleModals: true,
data: { data: {
organizationId: this.organizationId, organizationId: this.organization.id,
users: this.getCheckedUsers(), users: this.getCheckedUsers(),
isRevoking: isRevoking, isRevoking: isRevoking,
}, },
@ -352,7 +379,7 @@ export class PeopleComponent
try { try {
const request = new OrganizationUserBulkRequest(filteredUsers.map((user) => user.id)); const request = new OrganizationUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyOrganizationUserReinvite( const response = this.apiService.postManyOrganizationUserReinvite(
this.organizationId, this.organization.id,
request request
); );
this.showBulkStatus( this.showBulkStatus(
@ -376,7 +403,7 @@ export class PeopleComponent
BulkConfirmComponent, BulkConfirmComponent,
this.bulkConfirmModalRef, this.bulkConfirmModalRef,
(comp) => { (comp) => {
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.users = this.getCheckedUsers(); comp.users = this.getCheckedUsers();
} }
); );
@ -388,7 +415,7 @@ export class PeopleComponent
async events(user: OrganizationUserUserDetailsResponse) { async events(user: OrganizationUserUserDetailsResponse) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => { await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.entityId = user.id; comp.entityId = user.id;
comp.showUser = false; comp.showUser = false;
comp.entity = "user"; comp.entity = "user";
@ -402,7 +429,7 @@ export class PeopleComponent
(comp) => { (comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.email = user != null ? user.email : null; comp.email = user != null ? user.email : null;
comp.organizationId = this.organizationId; comp.organizationId = this.organization.id;
comp.id = user != null ? user.id : null; comp.id = user != null ? user.id : null;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil

View File

@ -1,10 +1,12 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { AccessSelectorModule } from "./components/access-selector"; import { AccessSelectorModule } from "./components/access-selector";
import { OrgUpgradeDialogComponent } from "./manage/org-upgrade-dialog/org-upgrade-dialog.component";
import { OrganizationsRoutingModule } from "./organization-routing.module"; import { OrganizationsRoutingModule } from "./organization-routing.module";
import { SharedOrganizationModule } from "./shared"; import { SharedOrganizationModule } from "./shared";
@NgModule({ @NgModule({
imports: [SharedOrganizationModule, AccessSelectorModule, OrganizationsRoutingModule], imports: [SharedOrganizationModule, AccessSelectorModule, OrganizationsRoutingModule],
declarations: [OrgUpgradeDialogComponent],
}) })
export class OrganizationModule {} export class OrganizationModule {}

View File

@ -5771,5 +5771,41 @@
}, },
"memberAccessAll": { "memberAccessAll": {
"message": "This member can access and modify all items." "message": "This member can access and modify all items."
},
"freeOrgInvLimitReachedManageBilling": {
"message": "Free organizations may have up to $SEATCOUNT$ members. Upgrade to a paid plan to invite more members.",
"placeholders": {
"seatcount": {
"content": "$1",
"example": "2"
}
}
},
"freeOrgInvLimitReachedNoManageBilling": {
"message": "Free organizations may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade.",
"placeholders": {
"seatcount": {
"content": "$1",
"example": "2"
}
}
},
"freeOrgMaxCollectionReachedManageBilling": {
"message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Upgrade to a paid plan to add more collections.",
"placeholders": {
"COLLECTIONCOUNT": {
"content": "$1",
"example": "2"
}
}
},
"freeOrgMaxCollectionReachedNoManageBilling": {
"message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Contact your organization owner to upgrade.",
"placeholders": {
"COLLECTIONCOUNT": {
"content": "$1",
"example": "2"
}
}
} }
} }

View File

@ -8,7 +8,7 @@ import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component"; import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive"; import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive"; import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({ @NgModule({
imports: [SharedModule, IconButtonModule, CdkDialogModule], imports: [SharedModule, IconButtonModule, CdkDialogModule],
@ -17,8 +17,15 @@ import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
DialogTitleContainerDirective, DialogTitleContainerDirective,
DialogComponent, DialogComponent,
SimpleDialogComponent, SimpleDialogComponent,
IconDirective,
],
exports: [
CdkDialogModule,
DialogComponent,
SimpleDialogComponent,
DialogCloseDirective,
IconDirective,
], ],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent, DialogCloseDirective],
providers: [DialogService], providers: [DialogService],
}) })
export class DialogModule {} export class DialogModule {}