1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-13811] Remove conditional code for extension refresh on web (#13145)

* Enable UI refresh on web by default

Removing all conditional code around the `ExtensionRefresh`-feature-flag on the web-UI

* Remove no longer needed extensRefresh helpers

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2025-02-10 14:20:05 +01:00
committed by GitHub
parent 7e2e604439
commit 9ddaf96020
15 changed files with 86 additions and 621 deletions

View File

@@ -22,7 +22,7 @@
[routerLink]="[]"
[queryParams]="{ itemId: cipher.id, action: clickAction }"
queryParamsHandling="merge"
[replaceUrl]="extensionRefreshEnabled"
[replaceUrl]="true"
title="{{ 'editItemWithName' | i18n: cipher.name }}"
type="button"
appStopProp

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -25,11 +22,6 @@ import { RowHeightClass } from "./vault-items.component";
export class VaultCipherRowComponent implements OnInit {
protected RowHeightClass = RowHeightClass;
/**
* Flag to determine if the extension refresh feature flag is enabled.
*/
protected extensionRefreshEnabled = false;
@Input() disabled: boolean;
@Input() cipher: CipherView;
@Input() showOwner: boolean;
@@ -61,19 +53,12 @@ export class VaultCipherRowComponent implements OnInit {
];
protected organization?: Organization;
constructor(
private configService: ConfigService,
private i18nService: I18nService,
) {}
constructor(private i18nService: I18nService) {}
/**
* Lifecycle hook for component initialization.
* Checks if the extension refresh feature flag is enabled to provide to template.
*/
async ngOnInit(): Promise<void> {
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
if (this.cipher.organizationId != null) {
this.organization = this.organizations.find((o) => o.id === this.cipher.organizationId);
}
@@ -83,7 +68,7 @@ export class VaultCipherRowComponent implements OnInit {
if (this.cipher.decryptionFailure) {
return "showFailedToDecrypt";
}
return this.extensionRefreshEnabled ? "view" : null;
return "view";
}
protected get showTotpCopyButton() {

View File

@@ -143,11 +143,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
}, 1000);
}
const extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card);
this.cardIsExpired = isCardExpired(this.cipher.card);
}
ngOnDestroy() {

View File

@@ -69,88 +69,48 @@
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<div appListDropdown>
<ng-container [ngSwitch]="extensionRefreshEnabled">
<ng-container *ngSwitchCase="true">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
<ng-container *ngSwitchCase="false">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
</ng-container>
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</div>
</div>
</app-header>

View File

@@ -9,13 +9,10 @@ import {
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Unassigned, CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -50,7 +47,6 @@ export class VaultHeaderComponent implements OnInit {
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
protected CipherType = CipherType;
protected extensionRefreshEnabled: boolean;
/**
* Boolean to determine the loading state of the header.
@@ -85,16 +81,9 @@ export class VaultHeaderComponent implements OnInit {
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
constructor(private i18nService: I18nService) {}
async ngOnInit() {
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
}
async ngOnInit() {}
/**
* The id of the organization that is currently being filtered on.

View File

@@ -22,12 +22,7 @@
<p class="tw-pl-1">
{{ "onboardingImportDataDetailsPartOne" | i18n }}
<button type="button" bitLink (click)="emitToAddCipher()">
{{
(extensionRefreshEnabled
? "onboardingImportDataDetailsLoginLink"
: "onboardingImportDataDetailsLink"
) | i18n
}}
{{ "onboardingImportDataDetailsLoginLink" | i18n }}
</button>
<span>
{{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}

View File

@@ -7,7 +7,6 @@ import { Subject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateProvider } from "@bitwarden/common/platform/state";
@@ -28,7 +27,6 @@ describe("VaultOnboardingComponent", () => {
let mockStateProvider: Partial<StateProvider>;
let setInstallExtLinkSpy: any;
let individualVaultPolicyCheckSpy: any;
let mockConfigService: MockProxy<ConfigService>;
beforeEach(() => {
mockPolicyService = mock<PolicyService>();
@@ -47,7 +45,6 @@ describe("VaultOnboardingComponent", () => {
}),
),
};
mockConfigService = mock<ConfigService>();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({
@@ -60,7 +57,6 @@ describe("VaultOnboardingComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: ApiService, useValue: mockApiService },
{ provide: StateProvider, useValue: mockStateProvider },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultOnboardingComponent);

View File

@@ -18,8 +18,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
@@ -58,14 +56,12 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
protected onboardingTasks$: Observable<VaultOnboardingTasks>;
protected showOnboarding = false;
protected extensionRefreshEnabled = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private apiService: ApiService,
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -74,9 +70,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
this.setInstallExtLink();
this.individualVaultPolicyCheck();
this.checkForBrowserExtension();
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnChanges(changes: SimpleChanges) {

View File

@@ -1,15 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -42,7 +34,6 @@ import {
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -57,9 +48,7 @@ import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -71,7 +60,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
@@ -105,13 +93,11 @@ import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
} from "./attachments-v2.component";
import { AttachmentsComponent } from "./attachments.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
@@ -160,15 +146,6 @@ const SearchTextDebounceInterval = 200;
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
trashCleanupWarning: string = null;
kdfIterations: number;
@@ -193,7 +170,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
private extensionRefreshEnabled: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -260,7 +236,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService,
private modalService: ModalService,
private dialogService: DialogService,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
@@ -278,7 +253,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private eventCollectionService: EventCollectionService,
private searchService: SearchService,
private searchPipe: SearchPipe,
private configService: ConfigService,
private apiService: ApiService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
@@ -437,15 +411,15 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => this.route.queryParams),
// Only process the queryParams if the dialog is not open (only when extension refresh is enabled)
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
// Only process the queryParams if the dialog is not open
filter(() => this.vaultItemDialogRef == undefined),
switchMap(async (params) => {
const cipherId = getCipherIdFromParams(params);
if (cipherId) {
if (await this.cipherService.get(cipherId)) {
let action = params.action;
// Default to "view" if extension refresh is enabled
if (action == null && this.extensionRefreshEnabled) {
// Default to "view"
if (action == null) {
action = "view";
}
@@ -544,11 +518,6 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refreshing = false;
},
);
// Check if the extension refresh feature flag is enabled
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
ngOnDestroy() {
@@ -642,8 +611,7 @@ export class VaultComponent implements OnInit, OnDestroy {
* Handles opening the attachments dialog for a cipher.
* Runs several checks to ensure that the user has the correct permissions
* and then opens the attachments dialog.
* Uses the new AttachmentsV2Component if the extensionRefresh feature flag is enabled.
*
* Uses the new AttachmentsV2Component
* @param cipher
* @returns
*/
@@ -668,51 +636,20 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
const canEditAttachments = await this.canEditAttachments(cipher);
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
});
let madeAttachmentChanges = false;
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
if (this.extensionRefreshEnabled) {
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
});
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
if (
result.action === AttachmentDialogResult.Uploaded ||
result.action === AttachmentDialogResult.Removed
) {
this.refresh();
}
return;
if (
result.action === AttachmentDialogResult.Uploaded ||
result.action === AttachmentDialogResult.Removed
) {
this.refresh();
}
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.viewOnly = !canEditAttachments;
comp.onUploadedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
comp.onReuploadedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
},
);
modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => {
if (madeAttachmentChanges) {
this.refresh();
}
madeAttachmentChanges = false;
});
return;
}
/**
@@ -751,48 +688,13 @@ export class VaultComponent implements OnInit, OnDestroy {
await this.go({ cipherId: null, itemId: null, action: null });
}
async addCipher(cipherType?: CipherType) {
const type = cipherType ?? this.activeFilter.cipherType;
if (this.extensionRefreshEnabled) {
return this.addCipherV2(type);
}
const component = (await this.editCipher(null)) as AddEditComponent;
component.type = type;
if (
this.activeFilter.organizationId !== "MyVault" &&
this.activeFilter.organizationId != null
) {
component.organizationId = this.activeFilter.organizationId;
component.collections = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).filter((c) => !c.readOnly && c.id != null);
}
const selectedColId = this.activeFilter.collectionId;
if (selectedColId !== "AllCollections" && selectedColId != null) {
const selectedCollection = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).find((c) => c.id === selectedColId);
component.organizationId = selectedCollection?.organizationId;
if (!selectedCollection.readOnly) {
component.collectionIds = [selectedColId];
}
}
component.folderId = this.activeFilter.folderId;
}
/**
* Opens the add cipher dialog.
* @param cipherType The type of cipher to add.
* @returns The dialog reference.
*/
async addCipherV2(cipherType?: CipherType) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"add",
null,
cipherType,
);
async addCipher(cipherType?: CipherType) {
const type = cipherType ?? this.activeFilter.cipherType;
const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type);
const collectionId =
this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null
? this.activeFilter.collectionId
@@ -823,6 +725,12 @@ export class VaultComponent implements OnInit, OnDestroy {
return this.editCipherId(cipher?.id, cloneMode);
}
/**
* Edit a cipher using the new VaultItemDialog.
* @param id
* @param cloneMode
* @returns
*/
async editCipherId(id: string, cloneMode?: boolean) {
const cipher = await this.cipherService.get(id);
@@ -836,49 +744,6 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (this.extensionRefreshEnabled) {
await this.editCipherIdV2(cipher, cloneMode);
return;
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = id;
comp.collectionId = this.selectedCollection?.node.id;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
},
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
modal.onClosedPromise().then(() => {
void this.go({ cipherId: null, itemId: null, action: null });
});
return childComponent;
}
/**
* Edit a cipher using the new VaultItemDialog.
*
* @param cipher
* @param cloneMode
*/
private async editCipherIdV2(cipher: Cipher, cloneMode?: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneMode ? "clone" : "edit",
cipher.id as CipherId,
@@ -1076,11 +941,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
const component = await this.editCipher(cipher, true);
if (component != null) {
component.cloneMode = true;
}
await this.editCipher(cipher, true);
}
restore = async (c: CipherView): Promise<boolean> => {
@@ -1331,15 +1192,6 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refresh$.next();
}
private async canEditAttachments(cipher: CipherView) {
if (cipher.organizationId == null || cipher.edit) {
return true;
}
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
return organization.canEditAllCiphers;
}
private async go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {

View File

@@ -104,8 +104,8 @@
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization"
class="tw-shrink-0"
>
<!-- "New" menu is always shown for Extension Refresh unless the user cannot create a cipher -->
<ng-container *ngIf="extensionRefreshEnabled && canCreateCipher; else nonRefresh">
<!-- "New" menu is always shown unless the user cannot create a cipher -->
<ng-container *ngIf="canCreateCipher">
<div appListDropdown>
<button
bitButton
@@ -145,56 +145,5 @@
</bit-menu>
</div>
</ng-container>
<ng-template #nonRefresh>
<!-- Show a menu when the user can create a cipher and collection -->
<div *ngIf="canCreateCipher && canCreateCollection" appListDropdown>
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</div>
<!-- Show a single button when the user can only create a cipher -->
<button
*ngIf="canCreateCipher && !canCreateCollection"
type="button"
bitButton
buttonType="primary"
(click)="addCipher()"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
<!-- Show a single button when the user can only create a collection -->
<button
*ngIf="canCreateCollection && !canCreateCipher"
type="button"
bitButton
buttonType="primary"
(click)="addCollection()"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newCollection" | i18n }}
</button>
</ng-template>
</div>
</app-header>

View File

@@ -13,7 +13,6 @@ import {
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -90,11 +89,6 @@ export class VaultHeaderComponent implements OnInit {
protected CollectionDialogTabType = CollectionDialogTabType;
/**
* Whether the extension refresh feature flag is enabled.
*/
protected extensionRefreshEnabled = false;
/** The cipher type enum. */
protected CipherType = CipherType;
@@ -106,11 +100,7 @@ export class VaultHeaderComponent implements OnInit {
private configService: ConfigService,
) {}
async ngOnInit() {
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnInit() {}
get title() {
const headerType = this.i18nService.t("collections").toLowerCase();

View File

@@ -1,15 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -37,12 +29,10 @@ import {
import {
CollectionAdminService,
CollectionAdminView,
CollectionService,
CollectionView,
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -127,7 +117,6 @@ import {
import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
import {
BulkCollectionsDialogComponent,
BulkCollectionsDialogResult,
@@ -166,13 +155,6 @@ enum AddAccessStatusType {
export class VaultComponent implements OnInit, OnDestroy {
protected Unassigned = Unassigned;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
@@ -210,7 +192,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
private extensionRefreshEnabled: boolean;
private resellerManagedOrgAlert: boolean;
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -249,7 +230,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private i18nService: I18nService,
private modalService: ModalService,
private dialogService: DialogService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
@@ -265,7 +245,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private eventCollectionService: EventCollectionService,
private totpService: TotpService,
private apiService: ApiService,
private collectionService: CollectionService,
private toastService: ToastService,
private configService: ConfigService,
private cipherFormConfigService: CipherFormConfigService,
@@ -278,10 +257,6 @@ export class VaultComponent implements OnInit, OnDestroy {
) {}
async ngOnInit() {
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
FeatureFlag.ResellerManagedOrgAlert,
);
@@ -555,7 +530,7 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => combineLatest([this.route.queryParams, allCipherMap$])),
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
filter(() => this.vaultItemDialogRef == undefined),
switchMap(async ([qParams, allCiphersMap]) => {
const cipherId = getCipherIdFromParams(qParams);
@@ -586,15 +561,15 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
// Default to "view" if extension refresh is enabled
if (action == null && this.extensionRefreshEnabled) {
// Default to "view"
if (action == null) {
action = "view";
}
if (action === "view") {
await this.viewCipherById(cipher);
} else {
await this.editCipherId(cipher, false);
await this.editCipher(cipher, false);
}
} else {
this.toastService.showToast({
@@ -836,27 +811,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
/** Opens the Add/Edit Dialog */
async addCipher(cipherType?: CipherType) {
if (this.extensionRefreshEnabled) {
return this.addCipherV2(cipherType);
}
let collections: CollectionView[] = [];
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(null, false, (comp) => {
comp.type = cipherType || this.activeFilter.cipherType;
comp.collections = collections;
if (this.activeFilter.collectionId) {
comp.collectionIds = [this.activeFilter.collectionId];
}
});
}
/** Opens the Add/Edit Dialog. Only to be used when the BrowserExtension feature flag is active */
async addCipherV2(cipherType?: CipherType) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"add",
null,
@@ -877,24 +833,8 @@ export class VaultComponent implements OnInit, OnDestroy {
* Edit the given cipher or add a new cipher
* @param cipherView - When set, the cipher to be edited
* @param cloneCipher - `true` when the cipher should be cloned.
* Used in place of the `additionalComponentParameters`, as
* the `editCipherIdV2` method has a differing implementation.
* @param defaultComponentParameters - A method that takes in an instance of
* the `AddEditComponent` to edit methods directly.
*/
async editCipher(
cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
return this.editCipherId(cipher, cloneCipher, additionalComponentParameters);
}
async editCipherId(
cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
async editCipher(cipher: CipherView | null, cloneCipher: boolean) {
if (
cipher &&
cipher.reprompt !== 0 &&
@@ -905,55 +845,6 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (this.extensionRefreshEnabled) {
await this.editCipherIdV2(cipher, cloneCipher);
return;
}
const defaultComponentParameters = (comp: AddEditComponent) => {
comp.organization = this.organization;
comp.organizationId = this.organization.id;
comp.cipherId = cipher?.id;
comp.collectionId = this.activeFilter.collectionId;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
};
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
additionalComponentParameters == null
? defaultComponentParameters
: (comp) => {
defaultComponentParameters(comp);
additionalComponentParameters(comp);
},
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
modal.onClosedPromise().then(() => {
this.go({ cipherId: null, itemId: null, action: null });
});
return childComponent;
}
/**
* Edit a cipher using the new AddEditCipherDialogV2 component.
* Only to be used behind the ExtensionRefresh feature flag.
*/
private async editCipherIdV2(cipher: CipherView | null, cloneCipher: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneCipher ? "clone" : "edit",
cipher?.id as CipherId | null,
@@ -1038,16 +929,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
let collections: CollectionView[] = [];
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(cipher, true, (comp) => {
comp.cloneMode = true;
comp.collections = collections;
comp.collectionIds = cipher.collectionIds;
});
await this.editCipher(cipher, true);
}
restore = async (c: CipherView): Promise<boolean> => {

View File

@@ -1,62 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { Navigation, Router, UrlTree } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { extensionRefreshRedirect } from "./extension-refresh-redirect";
describe("extensionRefreshRedirect", () => {
let configService: MockProxy<ConfigService>;
let router: MockProxy<Router>;
beforeEach(() => {
configService = mock<ConfigService>();
router = mock<Router>();
TestBed.configureTestingModule({
providers: [
{ provide: ConfigService, useValue: configService },
{ provide: Router, useValue: router },
],
});
});
it("returns true when ExtensionRefresh flag is disabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
const result = await TestBed.runInInjectionContext(() =>
extensionRefreshRedirect("/redirect")(),
);
expect(result).toBe(true);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
expect(router.parseUrl).not.toHaveBeenCalled();
});
it("returns UrlTree when ExtensionRefresh flag is enabled and preserves query params", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
const urlTree = new UrlTree();
urlTree.queryParams = { test: "test" };
const navigation: Navigation = {
extras: {},
id: 0,
initialUrl: new UrlTree(),
extractedUrl: urlTree,
trigger: "imperative",
previousNavigation: undefined,
};
router.getCurrentNavigation.mockReturnValue(navigation);
await TestBed.runInInjectionContext(() => extensionRefreshRedirect("/redirect")());
expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
queryParams: urlTree.queryParams,
});
});
});

View File

@@ -1,28 +0,0 @@
import { inject } from "@angular/core";
import { UrlTree, Router } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* Helper function to redirect to a new URL based on the ExtensionRefresh feature flag.
* @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled.
*/
export function extensionRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
return async () => {
const configService = inject(ConfigService);
const router = inject(Router);
const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
if (shouldRedirect) {
const currentNavigation = router.getCurrentNavigation();
const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
// Preserve query params when redirecting as it is likely that the refreshed component
// will be consuming the same query params.
return router.createUrlTree([redirectUrl], { queryParams });
} else {
return true;
}
};
}

View File

@@ -1,32 +0,0 @@
import { Type, inject } from "@angular/core";
import { Route, Routes } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { componentRouteSwap } from "./component-route-swap";
/**
* Helper function to swap between two components based on the ExtensionRefresh feature flag.
* @param defaultComponent - The current non-refreshed component to render.
* @param refreshedComponent - The new refreshed component to render.
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
* @param altOptions - The alt route options to apply to the alt component.
*/
export function extensionRefreshSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
altOptions?: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
refreshedComponent,
async () => {
const configService = inject(ConfigService);
return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
},
options,
altOptions,
);
}