1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-12681] Enable new Send Add/Edit Dialog on web (#13361)

* Create web-specific new-send-dropdown component

* Create web-specifc Send Add/Edit dialog

* Use new-send-dropdown and replace old Send Add/Edit with new Add/Edit dialog

* Delete old Send Add/Edit component

* Remove unused entries from en/messages.json

* Add cancel button to close dialog

* Remove unused RouterLink

* Fix typechecking issue

* Use observable to show/hide premium badge

* Add documentation

* Move assignment of observable into ctor, as it no longer requires a promise for assignment

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2025-03-04 16:09:37 +01:00
committed by GitHub
parent 0d68d22b98
commit bfbad99fb7
10 changed files with 393 additions and 471 deletions

View File

@@ -67,7 +67,6 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../tools/reports/pages/organizations/unsecured-websites-report.component";
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../tools/reports/pages/organizations/weak-passwords-report.component";
/* eslint no-restricted-imports: "error" */
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/individual-vault/add-edit.component";
@@ -148,7 +147,6 @@ import { SharedModule } from "./shared.module";
SecurityComponent,
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
@@ -212,7 +210,6 @@ import { SharedModule } from "./shared.module";
SecurityComponent,
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,

View File

@@ -1,286 +0,0 @@
<form
[formGroup]="formGroup"
[bitSubmit]="submitAndClose"
[appApiAction]="formPromise"
autocomplete="off"
>
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ title }}
</span>
<span bitDialogContent *ngIf="send">
<bit-callout *ngIf="disableSend">
{{ "sendDisabledWarning" | i18n }}
</bit-callout>
<bit-callout *ngIf="!disableSend && disableHideEmail">
{{ "sendOptionsPolicyInEffect" | i18n }}
<ul class="tw-mb-0">
<li>{{ "sendDisableHideEmailInEffect" | i18n }}</li>
</ul>
</bit-callout>
<bit-form-field class="tw-w-1/2">
<bit-label for="name">{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
<bit-hint>{{ "sendNameDesc" | i18n }}</bit-hint>
</bit-form-field>
<div class="tw-flex" *ngIf="!editMode">
<bit-radio-group formControlName="type">
<bit-label>{{ "whatTypeOfSend" | i18n }}</bit-label>
<bit-radio-button
*ngFor="let o of typeOptions"
id="type_{{ o.value }}"
class="tw-block"
[value]="o.value"
[disabled]="!canAccessPremium && o.premium"
>
<bit-label>
{{ o.name }}
<app-premium-badge
class="tw-mx-1"
*ngIf="!canAccessPremium && o.premium"
slot="end"
></app-premium-badge>
</bit-label>
</bit-radio-button>
</bit-radio-group>
</div>
<!-- Text -->
<ng-container *ngIf="type === sendType.Text">
<bit-form-field>
<bit-label for="text">{{ "sendTypeText" | i18n }}</bit-label>
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
<bit-hint>{{ "sendTextDesc" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="textHidden" />
<bit-label>{{ "textHiddenByDefault" | i18n }}</bit-label>
</bit-form-control>
</ng-container>
<!-- File -->
<ng-container *ngIf="type === sendType.File">
<div class="tw-flex">
<div *ngIf="editMode">
<bit-label>{{ "file" | i18n }}</bit-label>
<p bitTypography="body1" class="tw-mb-0">
{{ send.file.fileName }} ({{ send.file.sizeName }})
</p>
</div>
<bit-form-field *ngIf="!editMode">
<bit-label>{{ "file" | i18n }}</bit-label>
<div>
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
</div>
<input
bitInput
#fileSelector
type="file"
id="file"
name="file"
formControlName="file"
(change)="setSelectedFile($event)"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}</bit-hint>
</bit-form-field>
</div>
</ng-container>
<h4 bitTypography="h4" class="tw-mt-5">{{ "share" | i18n }}</h4>
<bit-form-field *ngIf="link">
<bit-label for="link">{{ "sendLinkLabel" | i18n }}</bit-label>
<input bitInput type="text" readonly formControlName="link" />
</bit-form-field>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="copyLink" />
<bit-label>{{ "copySendLinkOnSave" | i18n }}</bit-label>
</bit-form-control>
<div class="tw-mt-5 tw-flex" (click)="toggleOptions()">
<h4 bitTypography="h4" class="tw-mb-0 tw-mr-2">
<button type="button" bitLink appStopClick [attr.aria-expanded]="showOptions">
<i
class="bwi"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</h4>
</div>
<div id="options" [hidden]="!showOptions">
<div class="tw-flex">
<div *ngIf="!editMode" class="tw-w-1/2 tw-pr-3">
<bit-form-field>
<bit-label for="deletionDate">{{ "deletionDate" | i18n }}</bit-label>
<bit-select
id="deletionDate"
name="SelectedDeletionDatePreset"
formControlName="selectedDeletionDatePreset"
>
<bit-option
*ngFor="let o of deletionDatePresets"
[value]="o.value"
[label]="o.name"
></bit-option>
</bit-select>
<ng-container *ngIf="formGroup.controls['selectedDeletionDatePreset'].value === 0">
<input
bitInput
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
formControlName="defaultDeletionDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</ng-container>
<bit-hint>{{ "deletionDateDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div *ngIf="editMode" class="tw-w-1/2 tw-pr-3">
<bit-form-field>
<bit-label for="deletionDate">{{ "deletionDate" | i18n }}</bit-label>
<input
bitInput
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
formControlName="defaultDeletionDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<bit-hint>{{ "deletionDateDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div *ngIf="!editMode" class="tw-w-1/2 tw-pl-3">
<bit-form-field>
<bit-label for="expirationDate">
{{ "expirationDate" | i18n }}
</bit-label>
<bit-select
bitInput
id="expirationDate"
name="SelectedExpirationDatePreset"
formControlName="selectedExpirationDatePreset"
>
<bit-option
*ngFor="let e of expirationDatePresets"
[value]="e.value"
[label]="e.name"
></bit-option>
</bit-select>
<ng-container *ngIf="formGroup.controls['selectedExpirationDatePreset'].value === 0">
<input
bitInput
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
formControlName="defaultExpirationDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</ng-container>
<bit-hint>{{ "expirationDateDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div *ngIf="editMode" class="tw-w-1/2 tw-pl-3">
<bit-form-field>
<bit-label class="tw-flex" for="expirationDate">
{{ "expirationDate" | i18n }}
<button
type="button"
bitLink
appStopClick
(click)="clearExpiration()"
*ngIf="!disableSend"
class="tw-ml-auto"
slot="end"
>
{{ "clear" | i18n }}
</button>
</bit-label>
<input
bitInput
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
formControlName="defaultExpirationDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<bit-hint>{{ "expirationDateDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
</div>
<div class="tw-flex">
<bit-form-field class="tw-w-1/2 tw-pr-3">
<bit-label for="maxAccessCount">{{ "maxAccessCount" | i18n }}</bit-label>
<input bitInput type="number" formControlName="maxAccessCount" min="1" />
<bit-hint>{{ "maxAccessCountDesc" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-field class="tw-w-1/2 tw-pl-3" *ngIf="editMode">
<bit-label for="accessCount">{{ "currentAccessCount" | i18n }}</bit-label>
<input bitInput type="text" formControlName="accessCount" readonly />
</bit-form-field>
</div>
<div class="tw-flex">
<bit-form-field class="tw-w-1/2 tw-pr-3">
<bit-label for="password" *ngIf="!hasPassword">{{ "password" | i18n }}</bit-label>
<bit-label for="password" *ngIf="hasPassword">{{ "newPassword" | i18n }}</bit-label>
<input bitInput type="password" formControlName="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<bit-hint>{{ "sendPasswordDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<bit-form-field>
<bit-label>{{ "notes" | i18n }}</bit-label>
<textarea bitInput formControlName="notes" rows="6"></textarea>
<bit-hint>{{ "sendNotesDesc" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="hideEmail" />
<bit-label>{{ "hideEmail" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="disabled" />
<bit-label>{{ "disableThisSend" | i18n }}</bit-label>
</bit-form-control>
</div>
</span>
<ng-container bitDialogFooter>
<button
type="submit"
bitButton
bitFormButton
[appA11yTitle]="'save' | i18n"
buttonType="primary"
>
{{ "save" | i18n }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
[appA11yTitle]="'cancel' | i18n"
bitDialogClose
>
{{ "cancel" | i18n }}
</button>
<button
*ngIf="editMode"
type="button"
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
[appA11yTitle]="'delete' | i18n"
[bitAction]="deleteAndClose"
></button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -1,102 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { DatePipe } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
@Component({
selector: "app-send-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent {
override componentName = "app-send-add-edit";
protected selectedFile: File;
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
datePipe: DatePipe,
sendService: SendService,
stateService: StateService,
messagingService: MessagingService,
policyService: PolicyService,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService,
formBuilder: FormBuilder,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: { sendId: string },
accountService: AccountService,
toastService: ToastService,
) {
super(
i18nService,
platformUtilsService,
environmentService,
datePipe,
sendService,
messagingService,
policyService,
logService,
stateService,
sendApiService,
dialogService,
formBuilder,
billingAccountProfileStateService,
accountService,
toastService,
);
this.sendId = params.sendId;
}
async copyLinkToClipboard(link: string): Promise<void | boolean> {
// Copy function on web depends on the modal being open or not. Since this event occurs during a transition
// of the modal closing we need to add a small delay to make sure state of the DOM is consistent.
return new Promise((resolve) => {
window.setTimeout(() => resolve(super.copyLinkToClipboard(link)), 500);
});
}
protected setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
const file = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
this.selectedFile = file;
}
submitAndClose = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
const success = await this.submit();
if (success) {
this.dialogRef.close();
}
};
deleteAndClose = async () => {
const success = await this.delete();
if (success) {
this.dialogRef.close();
}
};
}

View File

@@ -0,0 +1,23 @@
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
<i *ngIf="!hideIcon" class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ (hideIcon ? "createSend" : "new") | i18n }}
</button>
<bit-menu #itemOptions>
<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,65 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Router } from "@angular/router";
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 { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { BadgeModule, ButtonModule, DialogService, MenuModule } from "@bitwarden/components";
import { DefaultSendFormConfigService } from "@bitwarden/send-ui";
import { SendAddEditComponent } from "../send-add-edit.component";
@Component({
selector: "tools-new-send-dropdown",
templateUrl: "new-send-dropdown.component.html",
standalone: true,
imports: [JslibModule, CommonModule, ButtonModule, MenuModule, BadgeModule],
providers: [DefaultSendFormConfigService],
})
/**
* 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>;
constructor(
private router: Router,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
private dialogService: DialogService,
private addEditFormConfigService: DefaultSendFormConfigService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
);
}
/**
* Opens the SendAddEditComponent for a new Send with the provided type.
* 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) {
return await this.router.navigate(["settings/subscription/premium"]);
}
const formConfig = await this.addEditFormConfigService.buildConfig("add", undefined, type);
await SendAddEditComponent.open(this.dialogService, { formConfig });
}
}

View File

@@ -0,0 +1,35 @@
<bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>
{{ headerText }}
</span>
<span bitDialogContent>
<tools-send-form
formId="sendForm"
[config]="config"
(onSendCreated)="onSendCreated($event)"
(onSendUpdated)="onSendUpdated($event)"
[submitBtn]="submitBtn"
>
</tools-send-form>
</span>
<ng-container bitDialogFooter>
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
<div class="tw-ml-auto">
<button
*ngIf="config?.mode !== 'add'"
type="button"
buttonType="danger"
slot="end"
bitIconButton="bwi-trash"
[bitAction]="deleteSend"
appA11yTitle="{{ 'delete' | i18n }}"
></button>
</div>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,176 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 {
AsyncActionsModule,
ButtonModule,
DialogService,
IconButtonModule,
SearchModule,
ToastService,
DialogModule,
} from "@bitwarden/components";
import { SendFormConfig, SendFormMode, SendFormModule } from "@bitwarden/send-ui";
export interface SendItemDialogParams {
/**
* The configuration object for the dialog and form.
*/
formConfig: SendFormConfig;
/**
* If true, the "edit" button will be disabled in the dialog.
*/
disableForm?: boolean;
}
export enum SendItemDialogResult {
/**
* A Send was saved (created or updated).
*/
Saved = "saved",
/**
* A Send was deleted.
*/
Deleted = "deleted",
}
/**
* Component for adding or editing a send item.
*/
@Component({
selector: "tools-send-add-edit",
templateUrl: "send-add-edit.component.html",
standalone: true,
imports: [
CommonModule,
SearchModule,
JslibModule,
FormsModule,
ButtonModule,
IconButtonModule,
SendFormModule,
AsyncActionsModule,
DialogModule,
],
})
export class SendAddEditComponent {
/**
* The header text for the component.
*/
headerText: string;
/**
* The configuration for the send form.
*/
config: SendFormConfig;
constructor(
@Inject(DIALOG_DATA) protected params: SendItemDialogParams,
private dialogRef: DialogRef<SendItemDialogResult>,
private i18nService: I18nService,
private sendApiService: SendApiService,
private toastService: ToastService,
private dialogService: DialogService,
) {
this.config = params.formConfig;
this.headerText = this.getHeaderText(this.config.mode, this.config.sendType);
}
/**
* Handles the event when the send is created.
*/
async onSendCreated(send: SendView) {
// FIXME Add dialogService.open send-created dialog
this.dialogRef.close(SendItemDialogResult.Saved);
return;
}
/**
* Handles the event when the send is updated.
*/
async onSendUpdated(send: SendView) {
this.dialogRef.close(SendItemDialogResult.Saved);
}
/**
* Handles the event when the send is deleted.
*/
async onSendDeleted() {
this.dialogRef.close(SendItemDialogResult.Deleted);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedSend"),
});
}
/**
* Handles the deletion of the current Send.
*/
deleteSend = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteSend" },
content: { key: "deleteSendPermanentConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
await this.sendApiService.delete(this.config.originalSend?.id);
} catch (e) {
this.toastService.showToast({
variant: "error",
title: null,
message: e.message,
});
return;
}
await this.onSendDeleted();
};
/**
* Gets the header text based on the mode and type.
* @param mode The mode of the send form.
* @param type The type of the send
* @returns The header text.
*/
private getHeaderText(mode: SendFormMode, type: SendType) {
const headerKey =
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
switch (type) {
case SendType.Text:
return this.i18nService.t(headerKey, this.i18nService.t("textSend"));
case SendType.File:
return this.i18nService.t(headerKey, this.i18nService.t("fileSend"));
}
}
/**
* Opens the send add/edit dialog.
* @param dialogService Instance of the DialogService.
* @param params The parameters for the dialog.
* @returns The dialog result.
*/
static open(dialogService: DialogService, params: SendItemDialogParams) {
return dialogService.open<SendItemDialogResult, SendItemDialogParams>(SendAddEditComponent, {
data: params,
});
}
}

View File

@@ -11,11 +11,7 @@
</ng-container>
</small>
</ng-container>
<button type="button" bitButton buttonType="primary" (click)="addSend()" [disabled]="disableSend">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "createSend" | i18n }}
</button>
<tools-new-send-dropdown *ngIf="!disableSend"></tools-new-send-dropdown>
</app-header>
<bit-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
@@ -198,10 +194,11 @@
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
<button slot="button" type="button" bitButton buttonType="secondary" (click)="addSend()">
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "createSend" | i18n }}
</button>
<tools-new-send-dropdown
[hideIcon]="true"
*ngIf="!disableSend"
slot="button"
></tools-new-send-dropdown>
</bit-no-items>
</ng-container>
</div>

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone, ViewChild, OnInit, OnDestroy, ViewContainerRef } from "@angular/core";
import { DialogRef } from "@angular/cdk/dialog";
import { Component, NgZone, OnInit, OnDestroy } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
@@ -14,6 +15,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 { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { SendId } from "@bitwarden/common/types/guid";
import {
DialogService,
NoItemsModule,
@@ -21,24 +23,25 @@ import {
TableDataSource,
ToastService,
} from "@bitwarden/components";
import { NoSendsIcon } from "@bitwarden/send-ui";
import { DefaultSendFormConfigService, NoSendsIcon, SendFormConfig } from "@bitwarden/send-ui";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { AddEditComponent } from "./add-edit.component";
import { NewSendDropdownComponent } from "./new-send/new-send-dropdown.component";
import { SendAddEditComponent, SendItemDialogResult } from "./send-add-edit.component";
const BroadcasterSubscriptionId = "SendComponent";
@Component({
selector: "app-send",
standalone: true,
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, NewSendDropdownComponent],
templateUrl: "send.component.html",
providers: [DefaultSendFormConfigService],
})
export class SendComponent extends BaseSendComponent implements OnInit, OnDestroy {
@ViewChild("sendAddEdit", { read: ViewContainerRef, static: true })
sendAddEditModalRef: ViewContainerRef;
private sendItemDialogRef?: DialogRef<SendItemDialogResult> | undefined;
noItemIcon = NoSendsIcon;
override set filteredSends(filteredSends: SendView[]) {
@@ -65,6 +68,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
sendApiService: SendApiService,
dialogService: DialogService,
toastService: ToastService,
private addEditFormConfigService: DefaultSendFormConfigService,
) {
super(
sendService,
@@ -111,17 +115,41 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
return;
}
await this.editSend(null);
const config = await this.addEditFormConfigService.buildConfig("add", null, 0);
await this.openSendItemDialog(config);
}
async editSend(send: SendView) {
const dialog = this.dialogService.open(AddEditComponent, {
data: {
sendId: send == null ? null : send.id,
},
const config = await this.addEditFormConfigService.buildConfig(
send == null ? "add" : "edit",
send == null ? null : (send.id as SendId),
send.type,
);
await this.openSendItemDialog(config);
}
/**
* Opens the send item dialog.
* @param formConfig The form configuration.
* */
async openSendItemDialog(formConfig: SendFormConfig) {
// Prevent multiple dialogs from being opened.
if (this.sendItemDialogRef) {
return;
}
this.sendItemDialogRef = SendAddEditComponent.open(this.dialogService, {
formConfig,
});
await lastValueFrom(dialog.closed);
await this.load();
const result = await lastValueFrom(this.sendItemDialogRef.closed);
this.sendItemDialogRef = undefined;
// If the dialog was closed by deleting the cipher, refresh the vault.
if (result === SendItemDialogResult.Deleted || result === SendItemDialogResult.Saved) {
await this.load();
}
}
}

View File

@@ -222,6 +222,9 @@
"notes": {
"message": "Notes"
},
"privateNote": {
"message": "Private note"
},
"note": {
"message": "Note"
},
@@ -5105,12 +5108,40 @@
"requireSsoExemption": {
"message": "Organization owners and admins are exempt from this policy's enforcement."
},
"limitSendViews": {
"message": "Limit views"
},
"limitSendViewsHint": {
"message": "No one can view this Send after the limit is reached.",
"description": "Displayed under the limit views field on Send"
},
"limitSendViewsCount": {
"message": "$ACCESSCOUNT$ views left",
"description": "Displayed under the limit views field on Send",
"placeholders": {
"accessCount": {
"content": "$1",
"example": "2"
}
}
},
"sendDetails": {
"message": "Send details",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeTextToShare": {
"message": "Text to share"
},
"sendTypeFile": {
"message": "File"
},
"sendTypeText": {
"message": "Text"
},
"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."
},
"createSend": {
"message": "New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -5135,19 +5166,15 @@
"message": "Delete Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "What type of Send is this?",
"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."
},
"deletionDate": {
"message": "Deletion date"
},
"deletionDateDesc": {
"message": "The Send will be permanently deleted on the specified date and time.",
"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."
},
"expirationDate": {
@@ -5160,21 +5187,6 @@
"maxAccessCount": {
"message": "Maximum access count"
},
"maxAccessCountDesc": {
"message": "If set, users will no longer be able to access this Send once the maximum access count is reached.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"currentAccessCount": {
"message": "Current access count"
},
"sendPasswordDesc": {
"message": "Optionally require a password for users to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNotesDesc": {
"message": "Private notes about this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disabled": {
"message": "Disabled"
},
@@ -5201,13 +5213,6 @@
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
},
"hideEmail": {
"message": "Hide my email address from recipients."
},
"disableThisSend": {
"message": "Deactivate this Send so that no one can access it.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"allSends": {
"message": "All Sends"
},
@@ -5218,6 +5223,9 @@
"pendingDeletion": {
"message": "Pending deletion"
},
"hideTextByDefault": {
"message": "Hide text by default"
},
"expired": {
"message": "Expired"
},
@@ -5439,13 +5447,6 @@
"message": "Always show members email address with recipients when creating or editing a Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptionsPolicyInEffect": {
"message": "The following organization policies are currently in effect:"
},
"sendDisableHideEmailInEffect": {
"message": "Users are not allowed to hide their email address from recipients when creating or editing a Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"modifiedPolicyId": {
"message": "Modified policy $ID$.",
"placeholders": {
@@ -5545,27 +5546,6 @@
"personalOwnershipCheckboxDesc": {
"message": "Remove individual ownership for organization users"
},
"textHiddenByDefault": {
"message": "When accessing the Send, hide the text by default",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNameDesc": {
"message": "A friendly name to describe this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTextDesc": {
"message": "The text you want to Send."
},
"sendFileDesc": {
"message": "The file you want to Send."
},
"copySendLinkOnSave": {
"message": "Copy the link to share this Send to my clipboard upon save."
},
"sendLinkLabel": {
"message": "Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"send": {
"message": "Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -5714,6 +5694,9 @@
"dateParsingError": {
"message": "There was an error saving your deletion and expiration dates."
},
"hideYourEmail": {
"message": "Hide your email address from viewers."
},
"webAuthnFallbackMsg": {
"message": "To verify your 2FA please click the button below."
},
@@ -9875,9 +9858,15 @@
"learnMoreAboutApi": {
"message": "Learn more about Bitwarden's API"
},
"fileSend": {
"message": "File Send"
},
"fileSends": {
"message": "File Sends"
},
"textSend": {
"message": "Text Send"
},
"textSends": {
"message": "Text Sends"
},