From 2f862b31e1aee1407dc3354acc73788768e819e4 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 22 Jan 2026 10:56:43 +0100 Subject: [PATCH] [PM-31029] Add feature flag for milestone 2 (#18458) * Add feature flag for milestone 2 * Fix test * Remove OnPush --- .../app/tools/send-v2/send-v2.component.html | 118 ++++++++++++++---- .../tools/send-v2/send-v2.component.spec.ts | 7 +- .../app/tools/send-v2/send-v2.component.ts | 118 +++++++++++++++--- apps/desktop/src/scss/migration.scss | 29 +++++ apps/desktop/src/scss/styles.scss | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 + 6 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 apps/desktop/src/scss/migration.scss diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.html b/apps/desktop/src/app/tools/send-v2/send-v2.component.html index eda740fa721..dad0e541a4d 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.html +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.html @@ -1,25 +1,93 @@ - - - @if (!disableSend()) { - - } - - - - - +@if (useDrawerEditMode()) { +
+ + + + @if (!disableSend()) { + + } + + + + + +
+} @else { + +
+
+ + + @if (!disableSend()) { + + } + +
+ + + + +
+
+ + + @if (action() == "add" || action() == "edit") { + + } + + + @if (!action()) { + + } +
+} diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts index a73a0534ff9..3670713f8f3 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts @@ -49,6 +49,7 @@ describe("SendV2Component", () => { let sendApiService: MockProxy; let toastService: MockProxy; let i18nService: MockProxy; + let configService: MockProxy; beforeEach(async () => { sendService = mock(); @@ -62,6 +63,10 @@ describe("SendV2Component", () => { sendApiService = mock(); toastService = mock(); i18nService = mock(); + configService = mock(); + + // 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(), }, { provide: MessagingService, useValue: mock() }, - { provide: ConfigService, useValue: mock() }, + { provide: ConfigService, useValue: configService }, { provide: ActivatedRoute, useValue: { diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts index 7fab0cb6702..95c0c971d2c 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -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(null); + protected readonly action = signal(Action.None); + private readonly selectedSendTypeOverride = signal(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 { - 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 { - 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 { + 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 { + await this.selectSend(send.id); + } + + protected async selectSend(sendId: string): Promise { + 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 { - await this.selectSend(send.id as SendId); + await this.selectSend(send.id); } protected async onCopySend(send: SendView): Promise { @@ -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(); + } } } diff --git a/apps/desktop/src/scss/migration.scss b/apps/desktop/src/scss/migration.scss new file mode 100644 index 00000000000..ba70d4fa009 --- /dev/null +++ b/apps/desktop/src/scss/migration.scss @@ -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"); + } +} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index c579e6acdc0..b4082afd38c 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -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"; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 9f6beb5f81e..f82c095d45f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -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,