1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00
Files
browser/apps/desktop/src/app/tools/send-v2/send-v2.component.ts
Isaac Ivins 316f4266dd Feature/pm 28788 desktop header UI migration (#18221)
Add desktop header component
2026-02-17 09:26:02 -08:00

254 lines
7.9 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { JslibModule } from "@bitwarden/angular/jslib.module";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.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 { 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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ButtonModule, DialogService, ToastService } from "@bitwarden/components";
import {
NewSendDropdownV2Component,
SendItemsService,
SendListComponent,
SendListState,
} 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: [
{
provide: PremiumUpgradePromptService,
useClass: DesktopPremiumUpgradePromptService,
},
],
templateUrl: "./send-v2.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendV2Component {
protected readonly addEditComponent = viewChild(AddEditComponent);
protected readonly sendId = signal<string | null>(null);
protected readonly action = signal<Action>(Action.None);
private readonly selectedSendTypeOverride = signal<SendType | undefined>(undefined);
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 cdr = inject(ChangeDetectorRef);
protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, {
initialValue: [],
});
protected readonly loading = toSignal(this.sendItemsService.loading$, { initialValue: true });
protected readonly currentSearchText = toSignal(this.sendItemsService.latestSearchText$, {
initialValue: "",
});
protected readonly disableSend = toSignal(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId),
),
),
{ initialValue: false },
);
protected readonly listState = toSignal(
combineLatest([
this.sendItemsService.emptyList$,
this.sendItemsService.noFilteredResults$,
]).pipe(
map(([emptyList, noFilteredResults]): SendListState | null => {
if (emptyList) {
return SendListState.Empty;
}
if (noFilteredResults) {
return SendListState.NoResults;
}
return null;
}),
),
{ initialValue: null },
);
constructor() {
// WORKAROUND: Force change detection when data updates
// This is needed because SendSearchComponent (shared lib) hasn't migrated to OnPush yet
// and doesn't trigger CD properly when search/add operations complete
// TODO: Remove this once SendSearchComponent migrates to OnPush (tracked in CL-764)
effect(() => {
this.filteredSends();
this.cdr.markForCheck();
});
}
protected async addSend(type: SendType): Promise<void> {
this.action.set(Action.Add);
this.sendId.set(null);
this.selectedSendTypeOverride.set(type);
const component = this.addEditComponent();
if (component) {
await component.resetAndLoad();
}
}
protected closeEditPanel(): void {
this.action.set(Action.None);
this.sendId.set(null);
this.selectedSendTypeOverride.set(undefined);
}
protected async savedSend(send: SendView): Promise<void> {
await this.selectSend(send.id);
}
protected async selectSend(sendId: string): Promise<void> {
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<void> {
await this.selectSend(send.id);
}
protected async onCopySend(send: SendView): Promise<void> {
const env = await this.environmentService.getEnvironment();
const link = env.getSendUrl() + send.accessId + "/" + send.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")),
});
}
protected async onRemovePassword(send: SendView): Promise<void> {
if (this.disableSend()) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "removePassword" },
content: { key: "removePasswordConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
await this.sendApiService.removePassword(send.id);
this.toastService.showToast({
variant: "success",
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);
}
}
protected async onDeleteSend(send: SendView): Promise<void> {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteSend" },
content: { key: "deleteSendConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
await this.sendApiService.delete(send.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedSend"),
});
this.closeEditPanel();
}
}