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:
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3439,6 +3439,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"new": {
|
||||
"message": "New"
|
||||
},
|
||||
"premium": {
|
||||
"message": "Premium"
|
||||
},
|
||||
"backTo": {
|
||||
"message": "Back to $NAME$",
|
||||
"description": "Navigate back to a previous folder or collection",
|
||||
|
||||
Reference in New Issue
Block a user