mirror of
https://github.com/bitwarden/browser
synced 2026-01-02 08:33:43 +00:00
[PM-17774] Build page for admin sponsored families (#14243)
* Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap * Add changes for the issue orgs * Added changes for the dialog * Rename the files properly * Remove the commented code * Change the implement to align with design * Add todo comments * Remove the comment todo * Fix the uni test error * Resolve the unit test * Resolve the unit test issue * Resolve the pr comments on any and route * remove the any * remove the generic validator * Resolve the unit test * Resolve the wrong message * Resolve the duplicate route --------- Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
This commit is contained in:
@@ -3,9 +3,10 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component";
|
||||
import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard";
|
||||
import { MembersComponent } from "./members.component";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -19,8 +20,8 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "sponsored-families",
|
||||
component: SponsoredFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab)],
|
||||
component: FreeBitwardenFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies],
|
||||
data: {
|
||||
titleId: "sponsoredFamilies",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router";
|
||||
import { firstValueFrom, switchMap, filter } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||
const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService);
|
||||
const organizationService = inject(OrganizationService);
|
||||
const accountService = inject(AccountService);
|
||||
|
||||
const org = accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => organizationService.organizations$(userId)),
|
||||
getById(route.params.organizationId),
|
||||
filter((org): org is Organization => org !== undefined),
|
||||
);
|
||||
|
||||
return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org));
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<form>
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "addSponsorship" | i18n }}</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<form [formGroup]="sponsorshipForm">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="email"
|
||||
formControlName="sponsorshipEmail"
|
||||
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="text"
|
||||
formControlName="sponsorshipNote"
|
||||
[attr.aria-invalid]="sponsorshipNoteControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="button" buttonType="primary" (click)="save()">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" [bitDialogClose]="false">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,135 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
interface RequestSponsorshipForm {
|
||||
sponsorshipEmail: FormControl<string | null>;
|
||||
sponsorshipNote: FormControl<string | null>;
|
||||
}
|
||||
|
||||
export interface AddSponsorshipDialogResult {
|
||||
action: AddSponsorshipDialogAction;
|
||||
value: Partial<AddSponsorshipFormValue> | null;
|
||||
}
|
||||
|
||||
interface AddSponsorshipFormValue {
|
||||
sponsorshipEmail: string;
|
||||
sponsorshipNote: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
enum AddSponsorshipDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "add-sponsorship-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
],
|
||||
})
|
||||
export class AddSponsorshipDialogComponent {
|
||||
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<AddSponsorshipDialogResult>,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
|
||||
sponsorshipEmail: new FormControl<string | null>("", {
|
||||
validators: [Validators.email, Validators.required],
|
||||
asyncValidators: [this.validateNotCurrentUserEmail.bind(this)],
|
||||
updateOn: "change",
|
||||
}),
|
||||
sponsorshipNote: new FormControl<string | null>("", {}),
|
||||
});
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService): DialogRef<AddSponsorshipDialogResult> {
|
||||
return dialogService.open<AddSponsorshipDialogResult>(AddSponsorshipDialogComponent);
|
||||
}
|
||||
|
||||
protected async save() {
|
||||
if (this.sponsorshipForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
// TODO: This is a mockup implementation - needs to be updated with actual API integration
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
const formValue = this.sponsorshipForm.getRawValue();
|
||||
const dialogValue: Partial<AddSponsorshipFormValue> = {
|
||||
status: "Sent",
|
||||
sponsorshipEmail: formValue.sponsorshipEmail ?? "",
|
||||
sponsorshipNote: formValue.sponsorshipNote ?? "",
|
||||
};
|
||||
|
||||
this.dialogRef.close({
|
||||
action: AddSponsorshipDialogAction.Saved,
|
||||
value: dialogValue,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null });
|
||||
};
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipEmail;
|
||||
}
|
||||
|
||||
get sponsorshipNoteControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipNote;
|
||||
}
|
||||
|
||||
private async validateNotCurrentUserEmail(
|
||||
control: AbstractControl,
|
||||
): Promise<ValidationErrors | null> {
|
||||
const value = control.value;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentUserEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")),
|
||||
);
|
||||
|
||||
if (!currentUserEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.toLowerCase() === currentUserEmail.toLowerCase()) {
|
||||
return { currentUserEmail: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<app-header>
|
||||
<button type="button" (click)="addSponsorship()" bitButton buttonType="primary">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addSponsorship" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
<bit-tab [label]="'sponsoredBitwardenFamilies' | i18n">
|
||||
<app-organization-sponsored-families
|
||||
[sponsoredFamilies]="sponsoredFamilies"
|
||||
(removeSponsorshipEvent)="removeSponsorhip($event)"
|
||||
></app-organization-sponsored-families>
|
||||
</bit-tab>
|
||||
|
||||
<bit-tab [label]="'memberFamilies' | i18n">
|
||||
<app-organization-member-families
|
||||
[memberFamilies]="sponsoredFamilies"
|
||||
></app-organization-member-families>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
<p class="tw-px-4" bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
import {
|
||||
AddSponsorshipDialogComponent,
|
||||
AddSponsorshipDialogResult,
|
||||
} from "./add-sponsorship-dialog.component";
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-free-bitwarden-families",
|
||||
templateUrl: "free-bitwarden-families.component.html",
|
||||
})
|
||||
export class FreeBitwardenFamiliesComponent implements OnInit {
|
||||
tabIndex = 0;
|
||||
sponsoredFamilies: SponsoredFamily[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.preventAccessToFreeFamiliesPage();
|
||||
}
|
||||
|
||||
async addSponsorship() {
|
||||
const addSponsorshipDialogRef: DialogRef<AddSponsorshipDialogResult> =
|
||||
AddSponsorshipDialogComponent.open(this.dialogService);
|
||||
|
||||
const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed);
|
||||
|
||||
if (dialogRef?.value) {
|
||||
this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies];
|
||||
}
|
||||
}
|
||||
|
||||
removeSponsorhip(sponsorship: any) {
|
||||
const index = this.sponsoredFamilies.findIndex(
|
||||
(e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail,
|
||||
);
|
||||
this.sponsoredFamilies.splice(index, 1);
|
||||
}
|
||||
|
||||
private async preventAccessToFreeFamiliesPage() {
|
||||
const showFreeFamiliesPage = await firstValueFrom(
|
||||
this.freeFamiliesPolicyService.showFreeFamilies$,
|
||||
);
|
||||
|
||||
if (!showFreeFamiliesPage) {
|
||||
await this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "membersWithSponsoredFamilies" | i18n }}
|
||||
</p>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "memberFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && memberFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of memberFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noMemberFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noMemberFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-member-families",
|
||||
templateUrl: "organization-member-families.component.html",
|
||||
})
|
||||
export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy {
|
||||
tabIndex = 0;
|
||||
loading = false;
|
||||
|
||||
@Input() memberFamilies: SponsoredFamily[] = [];
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "sponsorFreeBitwardenFamilies" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesInclude" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "sponsoredBitwardenFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && sponsoredFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "recipient" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "notes" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of sponsoredFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
<td bitCell>{{ o.sponsorshipNote }}</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
|
||||
<hr class="m-0" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'revokeAccount' | i18n"
|
||||
(click)="remove(o)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-close tw-text-danger"></i>
|
||||
<span class="tw-text-danger pl-1">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noSponsoredFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noSponsoredFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-sponsored-families",
|
||||
templateUrl: "organization-sponsored-families.component.html",
|
||||
})
|
||||
export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
tabIndex = 0;
|
||||
|
||||
@Input() sponsoredFamilies: SponsoredFamily[] = [];
|
||||
@Output() removeSponsorshipEvent = new EventEmitter<SponsoredFamily>();
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
remove(sponsorship: SponsoredFamily) {
|
||||
this.removeSponsorshipEvent.emit(sponsorship);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SponsoredFamily {
|
||||
sponsorshipEmail?: string;
|
||||
sponsorshipNote?: string;
|
||||
status?: string;
|
||||
}
|
||||
@@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization-
|
||||
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
|
||||
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
|
||||
|
||||
import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component";
|
||||
import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component";
|
||||
import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component";
|
||||
import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module";
|
||||
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||
import { SharedModule } from "./shared.module";
|
||||
@@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
@@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UpdatePasswordComponent,
|
||||
|
||||
Reference in New Issue
Block a user