1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

[PM-31029] Add feature flag for milestone 2 (#18458)

* Add feature flag for milestone 2

* Fix test

* Remove OnPush
This commit is contained in:
Oscar Hinton
2026-01-22 10:56:43 +01:00
committed by jaasen-livefront
parent 7533acb763
commit 2f862b31e1
6 changed files with 235 additions and 40 deletions

View File

@@ -1,25 +1,93 @@
<!-- Header with Send title and New button -->
<app-header>
@if (!disableSend()) {
<tools-new-send-dropdown-v2 buttonType="primary" (addSend)="addSend($event)" />
}
</app-header>
<!-- Send List Component -->
<tools-send-list
[sends]="filteredSends()"
[loading]="loading()"
[disableSend]="disableSend()"
[listState]="listState()"
[searchText]="currentSearchText()"
(editSend)="onEditSend($event)"
(copySend)="onCopySend($event)"
(deleteSend)="onDeleteSend($event)"
(removePassword)="onRemovePassword($event)"
>
<tools-new-send-dropdown-v2
slot="empty-button"
[hideIcon]="true"
buttonType="primary"
(addSend)="addSend($event)"
/>
</tools-send-list>
@if (useDrawerEditMode()) {
<div class="tw-m-4 tw-p-4">
<!-- New dialog-based layout (feature flag enabled) -->
<!-- Header with Send title and New button -->
<app-header>
@if (!disableSend()) {
<tools-new-send-dropdown-v2 buttonType="primary" (addSend)="addSend($event)" />
}
</app-header>
<!-- Send List Component -->
<tools-send-list
[sends]="filteredSends()"
[loading]="loading()"
[disableSend]="disableSend()"
[listState]="listState()"
[searchText]="currentSearchText()"
(editSend)="onEditSend($event)"
(copySend)="onCopySend($event)"
(deleteSend)="onDeleteSend($event)"
(removePassword)="onRemovePassword($event)"
>
<tools-new-send-dropdown-v2
slot="empty-button"
[hideIcon]="true"
buttonType="primary"
(addSend)="addSend($event)"
/>
</tools-send-list>
</div>
} @else {
<!-- Old split-panel layout (feature flag disabled) -->
<div id="sends" class="vault">
<div class="send-items-panel tw-w-2/5">
<!-- Header with Send title and New button -->
<app-header class="tw-block tw-pt-6 tw-px-6">
@if (!disableSend()) {
<button type="button" bitButton buttonType="primary" (click)="addSendWithoutType()">
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "new" | i18n }}
</button>
}
</app-header>
<div class="tw-mb-4 tw-px-6">
<!-- Send List Component -->
<tools-send-list
[sends]="filteredSends()"
[loading]="loading()"
[disableSend]="disableSend()"
[listState]="listState()"
[searchText]="currentSearchText()"
(editSend)="onEditSend($event)"
(copySend)="onCopySend($event)"
(deleteSend)="onDeleteSend($event)"
(removePassword)="onRemovePassword($event)"
>
<button
slot="empty-button"
type="button"
bitButton
buttonType="primary"
(click)="addSendWithoutType()"
>
{{ "newSend" | i18n }}
</button>
</tools-send-list>
</div>
</div>
<!-- Edit/Add panel (right side) -->
@if (action() == "add" || action() == "edit") {
<app-send-add-edit
id="addEdit"
class="details"
[sendId]="sendId()"
[type]="selectedSendType()"
(onSavedSend)="savedSend($event)"
(onCancelled)="closeEditPanel()"
(onDeletedSend)="closeEditPanel()"
></app-send-add-edit>
}
<!-- Bitwarden logo (shown when no send is selected) -->
@if (!action()) {
<div class="logo tw-w-1/2">
<div class="content">
<div class="inner-content">
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
</div>
</div>
</div>
}
</div>
}

View File

@@ -49,6 +49,7 @@ describe("SendV2Component", () => {
let sendApiService: MockProxy<SendApiService>;
let toastService: MockProxy<ToastService>;
let i18nService: MockProxy<I18nService>;
let configService: MockProxy<ConfigService>;
beforeEach(async () => {
sendService = mock<SendService>();
@@ -62,6 +63,10 @@ describe("SendV2Component", () => {
sendApiService = mock<SendApiService>();
toastService = mock<ToastService>();
i18nService = mock<I18nService>();
configService = mock<ConfigService>();
// Setup configService mock - feature flag returns true to test the new drawer mode
configService.getFeatureFlag$.mockReturnValue(of(true));
// Setup environmentService mock
environmentService.getEnvironment.mockResolvedValue({
@@ -117,7 +122,7 @@ describe("SendV2Component", () => {
useValue: mock<BillingAccountProfileStateService>(),
},
{ provide: MessagingService, useValue: mock<MessagingService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: ConfigService, useValue: configService },
{
provide: ActivatedRoute,
useValue: {

View File

@@ -1,11 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
effect,
inject,
signal,
viewChild,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { combineLatest, map, switchMap, lastValueFrom } from "rxjs";
@@ -15,6 +17,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -36,12 +40,27 @@ import {
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
import { DesktopHeaderComponent } from "../../layout/header";
import { AddEditComponent } from "../send/add-edit.component";
const Action = Object.freeze({
/** No action is currently active. */
None: "",
/** The user is adding a new Send. */
Add: "add",
/** The user is editing an existing Send. */
Edit: "edit",
} as const);
type Action = (typeof Action)[keyof typeof Action];
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-send-v2",
imports: [
JslibModule,
ButtonModule,
AddEditComponent,
SendListComponent,
NewSendDropdownV2Component,
DesktopHeaderComponent,
@@ -54,13 +73,19 @@ import { DesktopHeaderComponent } from "../../layout/header";
},
],
templateUrl: "./send-v2.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendV2Component {
protected readonly addEditComponent = viewChild(AddEditComponent);
protected readonly sendId = signal<string | null>(null);
protected readonly action = signal<Action>(Action.None);
private readonly selectedSendTypeOverride = signal<SendType | undefined>(undefined);
private sendFormConfigService = inject(DefaultSendFormConfigService);
private sendItemsService = inject(SendItemsService);
private policyService = inject(PolicyService);
private accountService = inject(AccountService);
private configService = inject(ConfigService);
private i18nService = inject(I18nService);
private platformUtilsService = inject(PlatformUtilsService);
private environmentService = inject(EnvironmentService);
@@ -70,6 +95,11 @@ export class SendV2Component {
private logService = inject(LogService);
private cdr = inject(ChangeDetectorRef);
protected readonly useDrawerEditMode = toSignal(
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone2),
{ initialValue: false },
);
protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, {
initialValue: [],
});
@@ -119,28 +149,79 @@ export class SendV2Component {
});
}
protected readonly selectedSendType = computed(() => {
const action = this.action();
const typeOverride = this.selectedSendTypeOverride();
if (action === Action.Add && typeOverride !== undefined) {
return typeOverride;
}
const sendId = this.sendId();
return this.filteredSends().find((s) => s.id === sendId)?.type;
});
protected async addSend(type: SendType): Promise<void> {
const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type);
if (this.useDrawerEditMode()) {
const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type);
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
formConfig,
});
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
formConfig,
});
await lastValueFrom(dialogRef.closed);
await lastValueFrom(dialogRef.closed);
} else {
this.action.set(Action.Add);
this.sendId.set(null);
this.selectedSendTypeOverride.set(type);
const component = this.addEditComponent();
if (component) {
await component.resetAndLoad();
}
}
}
protected async selectSend(sendId: SendId): Promise<void> {
const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId);
/** Used by old UI to add a send without specifying type (defaults to Text) */
protected async addSendWithoutType(): Promise<void> {
await this.addSend(SendType.Text);
}
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
formConfig,
});
protected closeEditPanel(): void {
this.action.set(Action.None);
this.sendId.set(null);
this.selectedSendTypeOverride.set(undefined);
}
await lastValueFrom(dialogRef.closed);
protected async savedSend(send: SendView): Promise<void> {
await this.selectSend(send.id);
}
protected async selectSend(sendId: string): Promise<void> {
if (this.useDrawerEditMode()) {
const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId as SendId);
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
formConfig,
});
await lastValueFrom(dialogRef.closed);
} else {
if (sendId === this.sendId() && this.action() === Action.Edit) {
return;
}
this.action.set(Action.Edit);
this.sendId.set(sendId);
const component = this.addEditComponent();
if (component) {
component.sendId = sendId;
await component.refresh();
}
}
}
protected async onEditSend(send: SendView): Promise<void> {
await this.selectSend(send.id as SendId);
await this.selectSend(send.id);
}
protected async onCopySend(send: SendView): Promise<void> {
@@ -176,6 +257,11 @@ export class SendV2Component {
title: null,
message: this.i18nService.t("removedPassword"),
});
if (!this.useDrawerEditMode() && this.sendId() === send.id) {
this.sendId.set(null);
await this.selectSend(send.id);
}
} catch (e) {
this.logService.error(e);
}
@@ -199,5 +285,9 @@ export class SendV2Component {
title: null,
message: this.i18nService.t("deletedSend"),
});
if (!this.useDrawerEditMode()) {
this.closeEditPanel();
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* Desktop UI Migration
*
* These are temporary styles during the desktop ui migration.
**/
/**
* This removes any padding applied by the bit-layout to content.
* This should be revisited once the table is migrated, and again once drawers are migrated.
**/
bit-layout {
#main-content {
padding: 0 0 0 0;
}
}
/**
* Send list panel styling for send-v2 component
* Temporary during migration - width handled by tw-w-2/5
**/
.vault > .send-items-panel {
order: 2;
min-width: 200px;
border-right: 1px solid;
@include themify($themes) {
background-color: themed("backgroundColor");
border-right-color: themed("borderColor");
}
}

View File

@@ -15,5 +15,6 @@
@import "left-nav.scss";
@import "loading.scss";
@import "plugins.scss";
@import "migration.scss";
@import "../../../../libs/angular/src/scss/icons.scss";
@import "../../../../libs/components/src/multi-select/scss/bw.theme";

View File

@@ -76,6 +76,7 @@ export enum FeatureFlag {
/* Desktop */
DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1",
DesktopUiMigrationMilestone2 = "desktop-ui-migration-milestone-2",
/* UIF */
RouterFocusManagement = "router-focus-management",
@@ -164,6 +165,7 @@ export const DefaultFeatureFlagValue = {
/* Desktop */
[FeatureFlag.DesktopUiMigrationMilestone1]: FALSE,
[FeatureFlag.DesktopUiMigrationMilestone2]: FALSE,
/* UIF */
[FeatureFlag.RouterFocusManagement]: FALSE,