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()) {
+
+
+
+
![Bitwarden]()
+
+
+
+ }
+
+}
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,