diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index bf2b26a11b..8cbbf48e27 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2090,6 +2090,9 @@
"passwordProtected": {
"message": "Password protected"
},
+ "copyLink": {
+ "message": "Copy link"
+ },
"copySendLink": {
"message": "Copy Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
index 3499f8c32e..52f7c3ed8f 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
@@ -8,14 +8,12 @@
-
+
{{ "sendsNoItemsTitle" | i18n }}
{{ "sendsNoItemsMessage" | i18n }}
+
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
new file mode 100644
index 0000000000..d7a302b790
--- /dev/null
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
@@ -0,0 +1,101 @@
+import { CommonModule } from "@angular/common";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { RouterLink } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { mock } from "jest-mock-extended";
+import { Observable, of } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+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";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+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 { ButtonModule, NoItemsModule } from "@bitwarden/components";
+import {
+ NewSendDropdownComponent,
+ SendListItemsContainerComponent,
+ SendListFiltersComponent,
+} from "@bitwarden/send-ui";
+
+import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
+import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
+import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
+
+import { SendV2Component } from "./send-v2.component";
+
+describe("SendV2Component", () => {
+ let component: SendV2Component;
+ let fixture: ComponentFixture
;
+ let sendViews$: Observable;
+
+ beforeEach(async () => {
+ sendViews$ = of([
+ { id: "1", name: "Send A" },
+ { id: "2", name: "Send B" },
+ ] as SendView[]);
+
+ await TestBed.configureTestingModule({
+ imports: [
+ CommonModule,
+ RouterTestingModule,
+ JslibModule,
+ NoItemsModule,
+ ButtonModule,
+ NoItemsModule,
+ RouterLink,
+ NewSendDropdownComponent,
+ SendListItemsContainerComponent,
+ SendListFiltersComponent,
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopOutComponent,
+ CurrentAccountComponent,
+ ],
+ providers: [
+ { provide: AccountService, useValue: mock() },
+ { provide: AuthService, useValue: mock() },
+ { provide: AvatarService, useValue: mock() },
+ {
+ provide: BillingAccountProfileStateService,
+ useValue: mock(),
+ },
+ { provide: ConfigService, useValue: mock() },
+ { provide: EnvironmentService, useValue: mock() },
+ { provide: LogService, useValue: mock() },
+ { provide: PlatformUtilsService, useValue: mock() },
+ { provide: SendApiService, useValue: mock() },
+ { provide: SendService, useValue: { sendViews$ } },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SendV2Component);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should sort sends by name on initialization", async () => {
+ const sortedSends = [
+ { id: "1", name: "Send A" },
+ { id: "2", name: "Send B" },
+ ] as SendView[];
+
+ await component.ngOnInit();
+
+ expect(component.sends).toEqual(sortedSends);
+ });
+});
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
index ebb014e4fc..6ee5f832be 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
@@ -1,13 +1,17 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { RouterLink } from "@angular/router";
+import { mergeMap, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
+import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
+import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { ButtonModule, NoItemsModule } from "@bitwarden/components";
import {
NoSendsIcon,
NewSendDropdownComponent,
+ SendListItemsContainerComponent,
SendListFiltersComponent,
} from "@bitwarden/send-ui";
@@ -16,10 +20,6 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
-enum SendsListState {
- Empty,
-}
-
@Component({
templateUrl: "send-v2.component.html",
standalone: true,
@@ -34,24 +34,31 @@ enum SendsListState {
ButtonModule,
RouterLink,
NewSendDropdownComponent,
+ SendListItemsContainerComponent,
SendListFiltersComponent,
],
})
export class SendV2Component implements OnInit, OnDestroy {
sendType = SendType;
- /** Visual state of the Sends list */
- protected sendsListState: SendsListState | null = null;
+ private destroy$ = new Subject();
+
+ sends: SendView[] = [];
protected noItemIcon = NoSendsIcon;
- protected SendsListStateEnum = SendsListState;
+ constructor(protected sendService: SendService) {}
- constructor() {
- this.sendsListState = SendsListState.Empty;
+ async ngOnInit() {
+ this.sendService.sendViews$
+ .pipe(
+ mergeMap(async (sends) => {
+ this.sends = sends.sort((a, b) => a.name.localeCompare(b.name));
+ }),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
}
- ngOnInit(): void {}
-
ngOnDestroy(): void {}
}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 9f1d15f648..2aed43787e 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -4386,6 +4386,9 @@
"message": "Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
+ "copyLink": {
+ "message": "Copy link"
+ },
"copySendLink": {
"message": "Copy Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
diff --git a/libs/tools/send/send-ui/src/index.ts b/libs/tools/send/send-ui/src/index.ts
index c85d646bbd..02326ac222 100644
--- a/libs/tools/send/send-ui/src/index.ts
+++ b/libs/tools/send/send-ui/src/index.ts
@@ -1,4 +1,5 @@
export * from "./icons";
export * from "./send-form";
export { NewSendDropdownComponent } from "./new-send-dropdown/new-send-dropdown.component";
+export { SendListItemsContainerComponent } from "./send-list-items-container/send-list-items-container.component";
export { SendListFiltersComponent } from "./send-list-filters/send-list-filters.component";
diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html
new file mode 100644
index 0000000000..6a4c6a308e
--- /dev/null
+++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html
@@ -0,0 +1,54 @@
+ 0">
+
+
+ {{ "allSends" | i18n }}
+
+ {{ sends.length }}
+
+
+
+
+
+
+
diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts
new file mode 100644
index 0000000000..847283ef5e
--- /dev/null
+++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts
@@ -0,0 +1,126 @@
+import { CommonModule } from "@angular/common";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { RouterTestingModule } from "@angular/router/testing";
+import { MockProxy, mock } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+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";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
+import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
+import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
+import {
+ ButtonModule,
+ BadgeModule,
+ DialogService,
+ IconButtonModule,
+ ItemModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ ToastService,
+ TypographyModule,
+} from "@bitwarden/components";
+
+import { SendListItemsContainerComponent } from "./send-list-items-container.component";
+
+describe("SendListItemsContainerComponent", () => {
+ let component: SendListItemsContainerComponent;
+ let fixture: ComponentFixture;
+ let environmentService: MockProxy;
+
+ const openSimpleDialog = jest.fn();
+ const showToast = jest.fn();
+ const copyToClipboard = jest.fn().mockImplementation(() => {});
+ const deleteFn = jest.fn().mockResolvedValue(undefined);
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ CommonModule,
+ RouterTestingModule,
+ JslibModule,
+ ItemModule,
+ ButtonModule,
+ BadgeModule,
+ IconButtonModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ TypographyModule,
+ ],
+ providers: [
+ { provide: EnvironmentService, useValue: environmentService },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: LogService, useValue: mock() },
+ { provide: PlatformUtilsService, useValue: { copyToClipboard } },
+ { provide: SendApiService, useValue: { delete: deleteFn } },
+ { provide: ToastService, useValue: { showToast } },
+ ],
+ })
+ .overrideProvider(DialogService, {
+ useValue: {
+ openSimpleDialog,
+ },
+ })
+ .compileComponents();
+
+ environmentService = mock();
+ Object.defineProperty(environmentService, "environment$", {
+ configurable: true,
+ get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
+ });
+
+ deleteFn.mockClear();
+ showToast.mockClear();
+ openSimpleDialog.mockClear();
+ copyToClipboard.mockClear();
+
+ fixture = TestBed.createComponent(SendListItemsContainerComponent);
+ component = fixture.componentInstance;
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should delete a send", async () => {
+ openSimpleDialog.mockResolvedValue(true);
+ const send = { id: "123", accessId: "abc", urlB64Key: "xyz" } as SendView;
+
+ await component.deleteSend(send);
+
+ expect(openSimpleDialog).toHaveBeenCalled();
+ expect(deleteFn).toHaveBeenCalledWith(send.id);
+ expect(showToast).toHaveBeenCalledWith({
+ variant: "success",
+ title: null,
+ message: "deletedSend",
+ });
+ });
+
+ it("should handle delete send cancellation", async () => {
+ const send = { id: "123", accessId: "abc", urlB64Key: "xyz" } as SendView;
+ openSimpleDialog.mockResolvedValue(false);
+
+ await component.deleteSend(send);
+
+ expect(openSimpleDialog).toHaveBeenCalled();
+ expect(deleteFn).not.toHaveBeenCalled();
+ expect(showToast).not.toHaveBeenCalled();
+ });
+
+ it("should copy send link", async () => {
+ const send = { id: "123", accessId: "abc", urlB64Key: "xyz" } as SendView;
+
+ await component.copySendLink(send);
+
+ expect(copyToClipboard).toHaveBeenCalledWith("https://example.com/#/send/abc/xyz");
+ expect(showToast).toHaveBeenCalledWith({
+ variant: "success",
+ title: null,
+ message: "valueCopied",
+ });
+ });
+});
diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts
new file mode 100644
index 0000000000..ef7232e97a
--- /dev/null
+++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts
@@ -0,0 +1,95 @@
+import { CommonModule } from "@angular/common";
+import { Component, Input } from "@angular/core";
+import { RouterLink } from "@angular/router";
+import { firstValueFrom } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+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";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
+import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
+import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
+import {
+ BadgeModule,
+ ButtonModule,
+ DialogService,
+ IconButtonModule,
+ ItemModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ ToastService,
+ TypographyModule,
+} from "@bitwarden/components";
+
+@Component({
+ imports: [
+ CommonModule,
+ ItemModule,
+ ButtonModule,
+ BadgeModule,
+ IconButtonModule,
+ SectionComponent,
+ TypographyModule,
+ JslibModule,
+ SectionHeaderComponent,
+ RouterLink,
+ ],
+ selector: "app-send-list-items-container",
+ templateUrl: "send-list-items-container.component.html",
+ standalone: true,
+})
+export class SendListItemsContainerComponent {
+ sendType = SendType;
+ /**
+ * The list of sends to display.
+ */
+ @Input()
+ sends: SendView[] = [];
+
+ constructor(
+ protected dialogService: DialogService,
+ protected environmentService: EnvironmentService,
+ protected i18nService: I18nService,
+ protected logService: LogService,
+ protected platformUtilsService: PlatformUtilsService,
+ protected sendApiService: SendApiService,
+ protected toastService: ToastService,
+ ) {}
+
+ async deleteSend(s: SendView): Promise {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "deleteSend" },
+ content: { key: "deleteSendConfirmation" },
+ type: "warning",
+ });
+
+ if (!confirmed) {
+ return false;
+ }
+
+ await this.sendApiService.delete(s.id);
+
+ try {
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("deletedSend"),
+ });
+ } catch (e) {
+ this.logService.error(e);
+ }
+ }
+
+ async copySendLink(s: SendView) {
+ const env = await firstValueFrom(this.environmentService.environment$);
+ const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key;
+ this.platformUtilsService.copyToClipboard(link);
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")),
+ });
+ }
+}