mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 02:33:33 +00:00
initial rework to hoist callout conditional to parent component
This commit is contained in:
@@ -1,28 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Optional, input } from "@angular/core";
|
||||
import { toObservable, takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Component, Optional, effect, input } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
defer,
|
||||
distinctUntilChanged,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
filter,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type.enum";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc/rxjs-operators";
|
||||
import { CalloutModule } from "@bitwarden/components";
|
||||
@@ -34,7 +21,7 @@ import { CalloutModule } from "@bitwarden/components";
|
||||
})
|
||||
export class ExportScopeCalloutComponent {
|
||||
show = false;
|
||||
scopeConfig!: {
|
||||
scopeConfig: {
|
||||
title: string;
|
||||
description: string;
|
||||
scopeIdentifier: string;
|
||||
@@ -44,96 +31,19 @@ export class ExportScopeCalloutComponent {
|
||||
readonly organizationId = input<string>();
|
||||
/* Optional export format, determines which individual export description to display */
|
||||
readonly exportFormat = input<string>();
|
||||
readonly showExcludeMyItems = input<boolean>(false);
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
@Optional() private router?: Router,
|
||||
) {
|
||||
const organizationId$ = toObservable(this.organizationId).pipe(distinctUntilChanged());
|
||||
const exportFormat$ = toObservable(this.exportFormat).pipe(distinctUntilChanged());
|
||||
|
||||
const activeAccount$ = this.accountService.activeAccount$;
|
||||
const userId$ = activeAccount$.pipe(getUserId);
|
||||
const email$ = activeAccount$.pipe(
|
||||
map((a) => a?.email ?? ""),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const defaultLocationFlag$ = defer(() =>
|
||||
from(this.configService.getFeatureFlag(FeatureFlag.CreateDefaultLocation)),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
const orgDataOwnershipEnforced$ = userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
userId == null
|
||||
? of(false)
|
||||
: this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest({
|
||||
organizationId: organizationId$,
|
||||
exportFormat: exportFormat$,
|
||||
userId: userId$,
|
||||
email: email$,
|
||||
defaultLocationFlagEnabled: defaultLocationFlag$,
|
||||
hasOwnership: orgDataOwnershipEnforced$,
|
||||
})
|
||||
.pipe(
|
||||
// organizationId is null for an individual vault export which actually doesn't require a userId (in the code below)
|
||||
// if organizationId is set, userId is required to get the org name for the organizational export
|
||||
filter(({ organizationId, userId }) => organizationId == null || userId != null),
|
||||
switchMap(
|
||||
({
|
||||
organizationId,
|
||||
exportFormat,
|
||||
userId,
|
||||
email,
|
||||
defaultLocationFlagEnabled,
|
||||
hasOwnership,
|
||||
}) => {
|
||||
const orgExportDescription =
|
||||
defaultLocationFlagEnabled && hasOwnership
|
||||
? this.isAdminConsoleContext
|
||||
? "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc"
|
||||
: "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc"
|
||||
: "exportingOrganizationVaultDesc";
|
||||
|
||||
if (organizationId != null) {
|
||||
// exporting from organizational vault
|
||||
return this.organizationService.organizations$(userId).pipe(
|
||||
getById(organizationId),
|
||||
map((org) => ({
|
||||
title: "exportingOrganizationVaultTitle",
|
||||
description: orgExportDescription,
|
||||
scopeIdentifier: org?.name ?? "",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// exporting from individual vault
|
||||
const description =
|
||||
exportFormat === "zip"
|
||||
? "exportingIndividualVaultWithAttachmentsDescription"
|
||||
: "exportingIndividualVaultDescription";
|
||||
|
||||
return of({
|
||||
title: "exportingPersonalVaultTitle",
|
||||
description,
|
||||
scopeIdentifier: email,
|
||||
});
|
||||
},
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((cfg) => {
|
||||
this.scopeConfig = cfg;
|
||||
this.show = true;
|
||||
});
|
||||
effect(async () => {
|
||||
this.show = false;
|
||||
await this.getScopeMessage(this.organizationId(), this.exportFormat());
|
||||
this.show = true;
|
||||
});
|
||||
}
|
||||
|
||||
private get isAdminConsoleContext(): boolean {
|
||||
@@ -148,4 +58,39 @@ export class ExportScopeCalloutComponent {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScopeMessage(organizationId: string, exportFormat: string): Promise<void> {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const orgExportDescription = this.showExcludeMyItems
|
||||
? this.isAdminConsoleContext
|
||||
? "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc"
|
||||
: "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc"
|
||||
: "exportingOrganizationVaultDesc";
|
||||
|
||||
if (organizationId != null) {
|
||||
// exporting from organizational vault
|
||||
const org = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(organizationId)),
|
||||
);
|
||||
|
||||
this.scopeConfig = {
|
||||
title: "exportingOrganizationVaultTitle",
|
||||
description: orgExportDescription,
|
||||
scopeIdentifier: org?.name ?? "",
|
||||
};
|
||||
} else {
|
||||
this.scopeConfig = {
|
||||
// exporting from individual vault
|
||||
title: "exportingPersonalVaultTitle",
|
||||
description:
|
||||
exportFormat === "zip"
|
||||
? "exportingIndividualVaultWithAttachmentsDescription"
|
||||
: "exportingIndividualVaultDescription",
|
||||
scopeIdentifier:
|
||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email)))) ??
|
||||
"",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<tools-export-scope-callout
|
||||
[organizationId]="organizationId"
|
||||
[exportFormat]="format"
|
||||
[showExcludeMyItems]="showExcludeMyItems"
|
||||
></tools-export-scope-callout>
|
||||
|
||||
<form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm">
|
||||
|
||||
@@ -13,11 +13,14 @@ import {
|
||||
} from "@angular/core";
|
||||
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
@@ -37,6 +40,8 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -85,6 +90,8 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
|
||||
})
|
||||
export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private _organizationId: OrganizationId | undefined;
|
||||
private createDefaultLocationFlagEnabled$: Observable<boolean>;
|
||||
private organizationExport$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
get organizationId(): OrganizationId | undefined {
|
||||
return this._organizationId;
|
||||
@@ -119,6 +126,33 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
private _showExcludeMyItems = false;
|
||||
|
||||
get showExcludeMyItems(): boolean {
|
||||
return this._showExcludeMyItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* When true, the callout will render a translation value specific to org data ownership policy
|
||||
* Default: false
|
||||
*
|
||||
* In order for this to be true, the CreateDefaultLocation feature flag must be enabled,
|
||||
* the Enforce Organization Data Ownership policy must be enabled,
|
||||
* and an organization must be selected as input (if organizationId is undefined, it's an individual export).
|
||||
*/
|
||||
@Input() set showExcludeMyItems(organizationId: OrganizationId) {
|
||||
// if organizationId is undefined or invalid, treat as an individual vault export, which is unaffected by policy/flag
|
||||
const validOrgId = !Utils.isNullOrEmpty(organizationId) && isId<OrganizationId>(organizationId);
|
||||
if (!validOrgId) {
|
||||
this._showExcludeMyItems = false;
|
||||
this.organizationExport$.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// this._organizationId = organizationId; // already assigned via the organizationId input setter
|
||||
this.organizationExport$.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method.
|
||||
* This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state.
|
||||
@@ -203,9 +237,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.createDefaultLocationFlagEnabled$ = from(
|
||||
this.configService.getFeatureFlag(FeatureFlag.CreateDefaultLocation),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
// Setup subscription to emit when this form is enabled/disabled
|
||||
this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
||||
this.formDisabled.emit(c === "DISABLED");
|
||||
@@ -225,6 +264,28 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
),
|
||||
);
|
||||
|
||||
/* determine value of _showExcludeMyItems in a reactive way
|
||||
feature flag is not expected to change at runtime
|
||||
policy and organization can be changed by user actions */
|
||||
combineLatest({
|
||||
createDefaultLocationFlagEnabled: this.createDefaultLocationFlagEnabled$,
|
||||
organizationDataOwnershipPolicyEnabled: this.organizationDataOwnershipPolicy$,
|
||||
hasOrganizationContext: this.organizationExport$,
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(
|
||||
({
|
||||
createDefaultLocationFlagEnabled,
|
||||
organizationDataOwnershipPolicyEnabled,
|
||||
hasOrganizationContext,
|
||||
}) => {
|
||||
this._showExcludeMyItems =
|
||||
hasOrganizationContext &&
|
||||
createDefaultLocationFlagEnabled &&
|
||||
organizationDataOwnershipPolicyEnabled;
|
||||
},
|
||||
);
|
||||
|
||||
this.exportForm.controls.vaultSelector.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value) => {
|
||||
|
||||
Reference in New Issue
Block a user