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()) { - - } -
+ + + + + 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";