1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Create desktop specific NewSendDropdown component

This commit is contained in:
Daniel James Smith
2025-03-12 10:54:25 +01:00
parent 15fa3cf08d
commit b96b33cb55
4 changed files with 182 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<button
bitButton
[bitMenuTriggerFor]="itemOptions"
buttonType="primary"
type="button"
class="tw-w-full"
>
<i *ngIf="!hideIcon" class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ (hideIcon ? "createSend" : "new") | i18n }}
</button>
<bit-menu #itemOptions class="tw-w-full">
<a bitMenuItem (click)="createSend(sendType.Text)">
<i class="bwi bwi-file-text" slot="start" aria-hidden="true"></i>
{{ "sendTypeText" | i18n }}
</a>
<a bitMenuItem (click)="createSend(sendType.File)">
<i class="bwi bwi-file" slot="start" aria-hidden="true"></i>
{{ "sendTypeFile" | i18n }}
<button
type="button"
slot="end"
*ngIf="!(canAccessPremium$ | async)"
bitBadge
variant="success"
>
{{ "premium" | i18n }}
</button>
</a>
</bit-menu>

View File

@@ -0,0 +1,86 @@
import { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { ButtonModule, MenuModule, BadgeModule } from "@bitwarden/components";
import { NewSendDropdownComponent } from "./new-send-dropdown.component";
describe("NewSendDropdownComponent", () => {
let component: NewSendDropdownComponent;
let fixture: ComponentFixture<NewSendDropdownComponent>;
let accountServiceMock: any;
let billingAccountProfileStateServiceMock: any;
let messagingServiceMock: any;
beforeEach(async () => {
accountServiceMock = {
activeAccount$: of({ id: "test-account-id" }),
};
billingAccountProfileStateServiceMock = {
hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(true)),
};
messagingServiceMock = {
send: jest.fn(),
};
await TestBed.configureTestingModule({
imports: [CommonModule, JslibModule, ButtonModule, MenuModule, BadgeModule],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: AccountService, useValue: accountServiceMock },
{
provide: BillingAccountProfileStateService,
useValue: billingAccountProfileStateServiceMock,
},
{ provide: MessagingService, useValue: messagingServiceMock },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NewSendDropdownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
it("should emit onCreateSendOfType when createSend is called with a valid type", async () => {
const sendType = SendType.Text;
jest.spyOn(component.onCreateSendOfType, "emit");
await component.createSend(sendType);
expect(component.onCreateSendOfType.emit).toHaveBeenCalledWith(sendType);
});
it("should call messagingService.send when createSend is called with SendType.File and no premium access", async () => {
billingAccountProfileStateServiceMock.hasPremiumFromAnySource$.mockReturnValue(of(false));
await component.createSend(SendType.File);
expect(messagingServiceMock.send).toHaveBeenCalledWith("openPremium");
});
it("should not call messagingService.send when createSend is called with SendType.File and has premium access", async () => {
const sendType = SendType.File;
jest.spyOn(component.onCreateSendOfType, "emit");
billingAccountProfileStateServiceMock.hasPremiumFromAnySource$.mockReturnValue(of(true));
await component.createSend(sendType);
expect(messagingServiceMock.send).not.toHaveBeenCalled();
expect(component.onCreateSendOfType.emit).toHaveBeenCalledWith(sendType);
});
});

View File

@@ -0,0 +1,61 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { firstValueFrom, Observable, of, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components";
@Component({
selector: "tools-new-send-dropdown",
templateUrl: "new-send-dropdown.component.html",
standalone: true,
imports: [JslibModule, CommonModule, ButtonModule, MenuModule, BadgeModule],
})
/**
* A dropdown component that allows the user to create a new Send of a specific type.
*/
export class NewSendDropdownComponent {
/** If true, the plus icon will be hidden */
@Input() hideIcon: boolean = false;
/** SendType provided for the markup to pass back the selected type of Send */
protected sendType = SendType;
/** Indicates whether the user can access premium features. */
protected canAccessPremium$: Observable<boolean>;
/** Emitted when an allowed SendType has been selected. */
@Output() onCreateSendOfType = new EventEmitter<SendType>();
constructor(
private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
private messagingService: MessagingService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
);
}
/**
* Emits an event with the user selected SendType, for the hosting control to launch the Add new Send page with the provided SendType.
* If has user does not have premium access and the type is File, the user will be redirected to the premium settings page.
* @param type The type of Send to create.
*/
async createSend(type: SendType) {
if (!(await firstValueFrom(this.canAccessPremium$)) && type === SendType.File) {
this.messagingService.send("openPremium");
return;
}
this.onCreateSendOfType.emit(type);
}
}

View File

@@ -3439,6 +3439,12 @@
}
}
},
"new": {
"message": "New"
},
"premium": {
"message": "Premium"
},
"backTo": {
"message": "Back to $NAME$",
"description": "Navigate back to a previous folder or collection",