diff --git a/apps/desktop/src/app/layout/header/desktop-header.component.html b/apps/desktop/src/app/layout/header/desktop-header.component.html
index efee5e21d9b..ae578312535 100644
--- a/apps/desktop/src/app/layout/header/desktop-header.component.html
+++ b/apps/desktop/src/app/layout/header/desktop-header.component.html
@@ -1,21 +1,19 @@
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
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 05c1332f1e7..eda740fa721 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,55 +1,25 @@
-
-
-
-
- @if (!disableSend()) {
-
- }
-
-
-
-
-
-
-
-
-
-
- @if (action() == "add" || action() == "edit") {
-
+
+
+ @if (!disableSend()) {
+
}
-
-
- @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 8a6e22cc402..a73a0534ff9 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
@@ -20,11 +20,16 @@ import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
+import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { DialogService, ToastService } from "@bitwarden/components";
-import { SendItemsService, SendListFiltersService } from "@bitwarden/send-ui";
-
-import { AddEditComponent } from "../send/add-edit.component";
+import {
+ SendItemsService,
+ SendListFiltersService,
+ DefaultSendFormConfigService,
+ SendAddEditDialogComponent,
+ SendFormConfig,
+} from "@bitwarden/send-ui";
import { SendV2Component } from "./send-v2.component";
@@ -37,12 +42,34 @@ describe("SendV2Component", () => {
let sendItemsService: MockProxy;
let sendListFiltersService: MockProxy;
let changeDetectorRef: MockProxy;
+ let sendFormConfigService: MockProxy;
+ let dialogService: MockProxy;
+ let environmentService: MockProxy;
+ let platformUtilsService: MockProxy;
+ let sendApiService: MockProxy;
+ let toastService: MockProxy;
+ let i18nService: MockProxy;
beforeEach(async () => {
sendService = mock();
accountService = mock();
policyService = mock();
changeDetectorRef = mock();
+ sendFormConfigService = mock();
+ dialogService = mock();
+ environmentService = mock();
+ platformUtilsService = mock();
+ sendApiService = mock();
+ toastService = mock();
+ i18nService = mock();
+
+ // Setup environmentService mock
+ environmentService.getEnvironment.mockResolvedValue({
+ getSendUrl: () => "https://send.bitwarden.com/#/",
+ } as any);
+
+ // Setup i18nService mock
+ i18nService.t.mockImplementation((key: string) => key);
// Mock SendItemsService with all required observables
sendItemsService = mock();
@@ -71,15 +98,16 @@ describe("SendV2Component", () => {
providers: [
provideNoopAnimations(),
{ provide: SendService, useValue: sendService },
- { provide: I18nService, useValue: mock() },
- { provide: PlatformUtilsService, useValue: mock() },
- { provide: EnvironmentService, useValue: mock() },
+ { provide: I18nService, useValue: i18nService },
+ { provide: PlatformUtilsService, useValue: platformUtilsService },
+ { provide: EnvironmentService, useValue: environmentService },
{ provide: SearchService, useValue: mockSearchService },
{ provide: PolicyService, useValue: policyService },
{ provide: LogService, useValue: mock() },
- { provide: SendApiService, useValue: mock() },
- { provide: DialogService, useValue: mock() },
- { provide: ToastService, useValue: mock() },
+ { provide: SendApiService, useValue: sendApiService },
+ { provide: DialogService, useValue: dialogService },
+ { provide: DefaultSendFormConfigService, useValue: sendFormConfigService },
+ { provide: ToastService, useValue: toastService },
{ provide: AccountService, useValue: accountService },
{ provide: SendItemsService, useValue: sendItemsService },
{ provide: SendListFiltersService, useValue: sendListFiltersService },
@@ -97,7 +125,16 @@ describe("SendV2Component", () => {
},
},
],
- }).compileComponents();
+ })
+ .overrideComponent(SendV2Component, {
+ set: {
+ providers: [
+ { provide: DefaultSendFormConfigService, useValue: sendFormConfigService },
+ { provide: PremiumUpgradePromptService, useValue: mock() },
+ ],
+ },
+ })
+ .compileComponents();
fixture = TestBed.createComponent(SendV2Component);
component = fixture.componentInstance;
@@ -107,103 +144,83 @@ describe("SendV2Component", () => {
expect(component).toBeTruthy();
});
- it("initializes with correct default action", () => {
- expect(component.action()).toBe("");
- });
-
describe("addSend", () => {
- it("sets action to Add", async () => {
- await component.addSend(SendType.Text);
- expect(component.action()).toBe("add");
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- it("calls resetAndLoad on addEditComponent when component exists", async () => {
- const mockAddEdit = mock();
- mockAddEdit.resetAndLoad.mockResolvedValue();
- jest.spyOn(component as any, "addEditComponent").mockReturnValue(mockAddEdit);
+ it("opens dialog with correct config for Text send", async () => {
+ const mockConfig = { mode: "add", sendType: SendType.Text } as SendFormConfig;
+ const mockDialogRef = { closed: of(true) };
- await component.addSend(SendType.Text);
+ sendFormConfigService.buildConfig.mockResolvedValue(mockConfig);
+ const openDrawerSpy = jest
+ .spyOn(SendAddEditDialogComponent, "openDrawer")
+ .mockReturnValue(mockDialogRef as any);
- expect(mockAddEdit.resetAndLoad).toHaveBeenCalled();
+ await component["addSend"](SendType.Text);
+
+ expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith(
+ "add",
+ undefined,
+ SendType.Text,
+ );
+ expect(openDrawerSpy).toHaveBeenCalled();
+ expect(openDrawerSpy.mock.calls[0][1]).toEqual({
+ formConfig: mockConfig,
+ });
});
- it("does not throw when addEditComponent is null", async () => {
- jest.spyOn(component as any, "addEditComponent").mockReturnValue(undefined);
- await expect(component.addSend(SendType.Text)).resolves.not.toThrow();
- });
- });
+ it("opens dialog with correct config for File send", async () => {
+ const mockConfig = { mode: "add", sendType: SendType.File } as SendFormConfig;
+ const mockDialogRef = { closed: of(true) };
- describe("closeEditPanel", () => {
- it("resets action to None", () => {
- component["action"].set("edit");
- component["sendId"].set("test-id");
+ sendFormConfigService.buildConfig.mockResolvedValue(mockConfig);
+ const openDrawerSpy = jest
+ .spyOn(SendAddEditDialogComponent, "openDrawer")
+ .mockReturnValue(mockDialogRef as any);
- component["closeEditPanel"]();
+ await component["addSend"](SendType.File);
- expect(component["action"]()).toBe("");
- expect(component["sendId"]()).toBeNull();
- });
- });
-
- describe("savedSend", () => {
- it("selects the saved send", async () => {
- jest.spyOn(component as any, "selectSend").mockResolvedValue();
-
- const mockSend = new SendView();
- mockSend.id = "saved-send-id";
-
- await component["savedSend"](mockSend);
-
- expect(component["selectSend"]).toHaveBeenCalledWith("saved-send-id");
+ expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith(
+ "add",
+ undefined,
+ SendType.File,
+ );
+ expect(openDrawerSpy).toHaveBeenCalled();
+ expect(openDrawerSpy.mock.calls[0][1]).toEqual({
+ formConfig: mockConfig,
+ });
});
});
describe("selectSend", () => {
- it("sets action to Edit and updates sendId", async () => {
- await component["selectSend"]("new-send-id");
-
- expect(component["action"]()).toBe("edit");
- expect(component["sendId"]()).toBe("new-send-id");
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- it("updates addEditComponent when it exists", async () => {
- const mockAddEdit = mock();
- mockAddEdit.refresh.mockResolvedValue();
- jest.spyOn(component as any, "addEditComponent").mockReturnValue(mockAddEdit);
+ it("opens dialog with correct config for editing send", async () => {
+ const mockConfig = { mode: "edit", sendId: "test-send-id" } as SendFormConfig;
+ const mockDialogRef = { closed: of(true) };
+
+ sendFormConfigService.buildConfig.mockResolvedValue(mockConfig);
+ const openDrawerSpy = jest
+ .spyOn(SendAddEditDialogComponent, "openDrawer")
+ .mockReturnValue(mockDialogRef as any);
await component["selectSend"]("test-send-id");
- expect(mockAddEdit.sendId).toBe("test-send-id");
- expect(mockAddEdit.refresh).toHaveBeenCalled();
- });
-
- it("does not reload if same send is already selected in edit mode", async () => {
- const mockAddEdit = mock();
- jest.spyOn(component as any, "addEditComponent").mockReturnValue(mockAddEdit);
- component["sendId"].set("same-id");
- component["action"].set("edit");
-
- await component["selectSend"]("same-id");
-
- expect(mockAddEdit.refresh).not.toHaveBeenCalled();
- });
-
- it("reloads if selecting different send", async () => {
- const mockAddEdit = mock();
- mockAddEdit.refresh.mockResolvedValue();
- jest.spyOn(component as any, "addEditComponent").mockReturnValue(mockAddEdit);
- component["sendId"].set("old-id");
- component["action"].set("edit");
-
- await component["selectSend"]("new-id");
-
- expect(mockAddEdit.refresh).toHaveBeenCalled();
+ expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith("edit", "test-send-id");
+ expect(openDrawerSpy).toHaveBeenCalled();
+ expect(openDrawerSpy.mock.calls[0][1]).toEqual({
+ formConfig: mockConfig,
+ });
});
});
describe("onEditSend", () => {
it("selects the send for editing", async () => {
- jest.spyOn(component as any, "selectSend").mockResolvedValue();
+ jest.spyOn(component as any, "selectSend").mockResolvedValue(undefined);
const mockSend = new SendView();
mockSend.id = "edit-send-id";
@@ -212,4 +229,25 @@ describe("SendV2Component", () => {
expect(component["selectSend"]).toHaveBeenCalledWith("edit-send-id");
});
});
+
+ describe("onCopySend", () => {
+ it("copies send link to clipboard and shows success toast", async () => {
+ const mockSend = {
+ accessId: "test-access-id",
+ urlB64Key: "test-key",
+ } as SendView;
+
+ await component["onCopySend"](mockSend);
+
+ expect(environmentService.getEnvironment).toHaveBeenCalled();
+ expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(
+ "https://send.bitwarden.com/#/test-access-id/test-key",
+ );
+ expect(toastService.showToast).toHaveBeenCalledWith({
+ variant: "success",
+ title: null,
+ message: expect.any(String),
+ });
+ });
+ });
});
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 be49e6593e4..7fab0cb6702 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
@@ -4,14 +4,11 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- computed,
effect,
inject,
- signal,
- viewChild,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
-import { combineLatest, map, switchMap } from "rxjs";
+import { combineLatest, map, switchMap, lastValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -25,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
+import { SendId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ButtonModule, DialogService, ToastService } from "@bitwarden/components";
import {
@@ -32,34 +30,24 @@ import {
SendItemsService,
SendListComponent,
SendListState,
+ SendAddEditDialogComponent,
+ DefaultSendFormConfigService,
} from "@bitwarden/send-ui";
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];
@Component({
selector: "app-send-v2",
imports: [
JslibModule,
ButtonModule,
- AddEditComponent,
SendListComponent,
NewSendDropdownV2Component,
DesktopHeaderComponent,
],
providers: [
+ DefaultSendFormConfigService,
{
provide: PremiumUpgradePromptService,
useClass: DesktopPremiumUpgradePromptService,
@@ -69,22 +57,17 @@ type Action = (typeof Action)[keyof typeof Action];
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 i18nService = inject(I18nService);
private platformUtilsService = inject(PlatformUtilsService);
private environmentService = inject(EnvironmentService);
- private logService = inject(LogService);
private sendApiService = inject(SendApiService);
private dialogService = inject(DialogService);
private toastService = inject(ToastService);
+ private logService = inject(LogService);
private cdr = inject(ChangeDetectorRef);
protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, {
@@ -137,53 +120,27 @@ export class SendV2Component {
}
protected async addSend(type: SendType): Promise {
- this.action.set(Action.Add);
- this.sendId.set(null);
- this.selectedSendTypeOverride.set(type);
+ const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type);
- const component = this.addEditComponent();
- if (component) {
- await component.resetAndLoad();
- }
+ const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
+ formConfig,
+ });
+
+ await lastValueFrom(dialogRef.closed);
}
- protected closeEditPanel(): void {
- this.action.set(Action.None);
- this.sendId.set(null);
- this.selectedSendTypeOverride.set(undefined);
+ protected async selectSend(sendId: SendId): Promise {
+ const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId);
+
+ const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
+ formConfig,
+ });
+
+ await lastValueFrom(dialogRef.closed);
}
- protected async savedSend(send: SendView): Promise {
- await this.selectSend(send.id);
- }
-
- protected async selectSend(sendId: string): Promise {
- 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 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 onEditSend(send: SendView): Promise {
- await this.selectSend(send.id);
+ await this.selectSend(send.id as SendId);
}
protected async onCopySend(send: SendView): Promise {
@@ -219,11 +176,6 @@ export class SendV2Component {
title: null,
message: this.i18nService.t("removedPassword"),
});
-
- if (this.sendId() === send.id) {
- this.sendId.set(null);
- await this.selectSend(send.id);
- }
} catch (e) {
this.logService.error(e);
}
@@ -247,7 +199,5 @@ export class SendV2Component {
title: null,
message: this.i18nService.t("deletedSend"),
});
-
- this.closeEditPanel();
}
}
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 33de901c06b..56eacc94e50 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -100,6 +100,70 @@
}
}
},
+ "deletionDateDescV2": {
+ "message": "The Send will be permanently deleted on this date.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "fileToShare": {
+ "message": "File to share"
+ },
+ "hideTextByDefault": {
+ "message": "Hide text by default"
+ },
+ "hideYourEmail": {
+ "message": "Hide your email address from viewers."
+ },
+ "limitSendViews": {
+ "message": "Limit views"
+ },
+ "limitSendViewsCount": {
+ "message": "$ACCESSCOUNT$ views left",
+ "description": "Displayed under the limit views field on Send",
+ "placeholders": {
+ "accessCount": {
+ "content": "$1",
+ "example": "2"
+ }
+ }
+ },
+ "limitSendViewsHint": {
+ "message": "No one can view this Send after the limit is reached.",
+ "description": "Displayed under the limit views field on Send"
+ },
+ "privateNote": {
+ "message": "Private note"
+ },
+ "sendDetails": {
+ "message": "Send details",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendPasswordDescV3": {
+ "message": "Add an optional password for recipients to access this Send.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendTypeTextToShare": {
+ "message": "Text to share"
+ },
+ "newItemHeaderTextSend": {
+ "message": "New Text Send",
+ "description": "Header for new text send"
+ },
+ "newItemHeaderFileSend": {
+ "message": "New File Send",
+ "description": "Header for new file send"
+ },
+ "editItemHeaderTextSend": {
+ "message": "Edit Text Send",
+ "description": "Header for edit text send"
+ },
+ "editItemHeaderFileSend": {
+ "message": "Edit File Send",
+ "description": "Header for edit file send"
+ },
+ "deleteSendPermanentConfirmation": {
+ "message": "Are you sure you want to permanently delete this Send?",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
"new": {
"message": "New",
"description": "for adding new items"
diff --git a/apps/desktop/src/scss/migration.scss b/apps/desktop/src/scss/migration.scss
deleted file mode 100644
index ba70d4fa009..00000000000
--- a/apps/desktop/src/scss/migration.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * 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 b4082afd38c..c579e6acdc0 100644
--- a/apps/desktop/src/scss/styles.scss
+++ b/apps/desktop/src/scss/styles.scss
@@ -15,6 +15,5 @@
@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";