1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-06-28 10:55:27 +02:00

[PM-2805] Migrate add edit send to Component Library (#6004)

* Converted add-edit send component dialog into a bit-dialog

* Updated Send AddEdit text fields to Component Library

* Migrated Share and Options fields to ComponentLibrary on SendAddEdit

* Migrated footer buttons to ComponentLibrary on SendAddEdit

* Updated web's SendAddEdit component file fields

* Replaced file upload with component library

* Changed SendAddEdit to use Reactive Forms on web

* Changed browser SendAddEdit to use ReactiveForms

* Update SendAddEdit on desktop to use ReactiveForms

* Added AppA11yTitle to button on web SendAddEdit

* Initial efflux-dates web change to ComponentLibrary

* Corrected delete button to check if it is in EditMode on SendAddEdit

* Using BitLink on options button

* Corrected typo on send add edit desktop

* Replaced efflux-dates with datetime-local input on SendAddEdit web, browser and desktop

* Removed efflux dates

* Added firefox custom date popout message on DeletionDate to SendAddEdit browser component

* moved desktop's new send data reload from send to SendAddEdit component

* removing unnecessary attributes and spans from Send AddEdit web

* removed redundant try catch from add edit and unnecessary parameter from close

* Added type for date select options

* Removed unnecessary classes and swapped bootstrap classes by corresponding tailwind classes

* Removed unnecessary code

* Added file as required field
Submit only closes popup on success

* Added pre validations at start of submit

* PM-3668 removed expiration date from required

* PM-3671 not defaulting maximum access count to 0

* PM-3669 Copying the link from link method

* Removed required tag from html and added to formgroup

* PM-3679 Checking if is not EditMode before validating if FormGroup file value is set

* PM-3691 Moved error validation to web component as browser and desktop need to show popup error

* PM-3696 - Disabling hide email when it is unset and has policy to not allow hiding

* PM-3694 - Properly setting default value for dates on Desktop when changing from an existing send

* Disabling hidden required fields

* [PM-3800] Clearing password on new send
This commit is contained in:
aj-rosado 2023-09-07 13:49:13 +01:00 committed by GitHub
parent 86bdfaa7ba
commit 5f78aeaef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 777 additions and 1388 deletions

View File

@ -33,7 +33,6 @@ import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
import { SendListComponent } from "../tools/popup/send/components/send-list.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "../tools/popup/send/efflux-dates.component";
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
@ -133,7 +132,6 @@ import "../platform/popup/locales";
PrivateModeWarningComponent,
RegisterComponent,
SendAddEditComponent,
SendEffluxDatesComponent,
SendGroupingsComponent,
SendListComponent,
SendTypeComponent,

View File

@ -1,217 +0,0 @@
<ng-container [formGroup]="datesForm">
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" appBoxRow>
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
aria-describedby="deletionDateHelp"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row" appBoxRow *ngIf="selectedDeletionDatePreset.value === 0">
<ng-container *ngTemplateOutlet="deletionDateCustom"></ng-container>
</div>
</ng-container>
<div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<ng-container *ngTemplateOutlet="deletionDateCustom"></ng-container>
</div>
</div>
<div id="deletionDateHelp" class="box-footer">
{{ "deletionDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
browserPath == 'firefox' &&
(editMode || (selectedDeletionDatePreset.value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow.emit()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="editExpirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="ExpirationDateSelect"
aria-describedby="expirationDateHelp"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div class="box-content-row" *ngIf="selectedExpirationDatePreset.value === 0" appBoxRow>
<ng-container *ngTemplateOutlet="expirationDateCustom"></ng-container>
</div>
</ng-container>
<div class="box-content-row" *ngIf="editMode" appBoxRow>
<div class="flex-label">
<label>{{ "expirationDate" | i18n }}</label>
<button type="button" *ngIf="!disabled" appStopClick (click)="clearExpiration()">
{{ "clear" | i18n }}
</button>
</div>
<ng-container *ngTemplateOutlet="expirationDateCustom"></ng-container>
</div>
</div>
<div id="expirationDateHelp" class="box-footer">
{{ "expirationDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
browserPath == 'firefox' &&
(editMode || (selectedExpirationDatePreset.value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow.emit()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<ng-template #deletionDateCustom>
<ng-container [ngSwitch]="browserPath">
<ng-container *ngSwitchCase="'firefox'">
<div class="flex flex-grow">
<input
id="deletionDateCustomFallback"
type="date"
name="DeletionDateFallback"
aria-describedby="deletionDateHelp"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<input
id="deletionTimeCustomFallback"
type="time"
name="DeletionTimeDate"
formControlName="fallbackDeletionTime"
required
placeholder="HH:MM AM/PM"
[readOnly]="disabled"
/>
</div>
</ng-container>
<ng-container *ngSwitchCase="'safari'">
<div class="flex flex-grow">
<input
id="deletionDateCustomFallback"
type="date"
name="DeletionDateFallback"
aria-describedby="deletionDateHelp"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<select
id="deletionTimeCustomFallback"
formControlName="fallbackDeletionTime"
name="SafariDeletionTime"
>
<option *ngFor="let o of safariDeletionTimePresetOptions" [ngValue]="o.twentyFourHour">
{{ o.twelveHour }}
</option>
</select>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
<input
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
aria-describedby="deletionDateHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</ng-container>
</ng-container>
</ng-template>
<ng-template #expirationDateCustom>
<ng-container [ngSwitch]="browserPath">
<ng-container *ngSwitchCase="'firefox'">
<div class="flex flex-grow">
<input
id="expirationDateCustomFallback"
type="date"
name="ExpirationDateFallback"
aria-describedby="expirationDateHelp"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<input
id="expirationTimeCustomFallback"
type="time"
name="ExpirationTimeFallback"
formControlName="fallbackExpirationTime"
[required]="!editMode"
placeholder="HH:MM AM/PM"
[readOnly]="disabled"
/>
</div>
</ng-container>
<ng-container *ngSwitchCase="'safari'">
<div class="flex flex-grow">
<input
id="expirationDateCustomFallback"
type="date"
name="ExpirationDateFallback"
aria-describedby="expirationDateHelp"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<select
id="expirationTimeCustomFallback"
formControlName="fallbackExpirationTime"
name="SafariExpirationTime"
>
<option
*ngFor="let o of safariExpirationTimePresetOptions"
[ngValue]="o.twentyFourHour"
>
{{ o.twelveHour }}
</option>
</select>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
<input
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
aria-describedby="expirationDateHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disabled"
/>
</ng-container>
</ng-container>
</ng-template>
</ng-container>

View File

@ -1,25 +0,0 @@
import { DatePipe } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ControlContainer, NgForm } from "@angular/forms";
import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({
selector: "app-send-efflux-dates",
templateUrl: "efflux-dates.component.html",
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class EffluxDatesComponent extends BaseEffluxDatesComponent {
@Input() readonly inPopout: boolean;
@Output() popOutWindow = new EventEmitter();
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {
super(i18nService, platformUtilsService, datePipe);
}
}

View File

@ -1,4 +1,4 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
@ -7,7 +7,7 @@
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading || disableSend">
<button type="submit">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
@ -42,9 +42,8 @@
<input
id="name"
type="text"
name="Name"
formControlName="name"
aria-describedby="nameHelp"
[(ngModel)]="send.name"
[readonly]="disableSend"
/>
</div>
@ -66,12 +65,9 @@
>
<input
type="radio"
[(ngModel)]="send.type"
name="Type_{{ o.value }}"
formControlName="type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="send.type === o.value"
[readonly]="disableSend"
/>
<label for="type_{{ o.value }}">
@ -82,7 +78,7 @@
</div>
</div>
<!-- File -->
<div class="box" *ngIf="send.type === sendType.File && (editMode || showFileSelector)">
<div class="box" *ngIf="type === sendType.File && (editMode || showFileSelector)">
<div class="box-content no-hover">
<div class="box-content-row" *ngIf="editMode">
<label for="file">{{ "file" | i18n }}</label>
@ -93,9 +89,8 @@
<input
type="file"
id="file"
name="file"
formControlName="file"
aria-describedby="fileHelp"
required
[readonly]="disableSend"
/>
</div>
@ -105,17 +100,15 @@
</div>
</div>
<!-- Text -->
<div class="box" *ngIf="send.type === sendType.Text">
<div class="box" *ngIf="type === sendType.Text">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="text">{{ "sendTypeText" | i18n }}</label>
<textarea
id="text"
name="Text"
formControlName="text"
aria-describedby="textHelp"
rows="6"
[(ngModel)]="send.text.text"
[readonly]="disableSend"
></textarea>
</div>
</div>
@ -125,13 +118,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideText">{{ "sendHideText" | i18n }}</label>
<input
id="hideText"
type="checkbox"
name="HideText"
[(ngModel)]="send.text.hidden"
[disabled]="disableSend"
/>
<input id="hideText" type="checkbox" name="HideText" formControlName="textHidden" />
</div>
</div>
</div>
@ -144,13 +131,7 @@
<!-- Copy Link on Save -->
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="copyOnSave">{{ "sendShareDesc" | i18n }}</label>
<input
id="copyOnSave"
type="checkbox"
name="CopyOnSave"
[(ngModel)]="copyLink"
[disabled]="disableSend"
/>
<input id="copyOnSave" type="checkbox" name="CopyOnSave" formControlName="copyLink" />
</div>
</div>
</div>
@ -170,15 +151,140 @@
</h2>
</div>
<div [hidden]="!showOptions">
<app-send-efflux-dates
[initialDeletionDate]="send.deletionDate"
[initialExpirationDate]="send.expirationDate"
[editMode]="editMode"
[disabled]="disableSend"
(datesChanged)="setDates($event)"
(popOutWindow)="popOutWindow()"
>
</app-send-efflux-dates>
<!-- Deletion Date -->
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" appBoxRow>
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
aria-describedby="deletionDateHelp"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div
class="box-content-row"
appBoxRow
*ngIf="formGroup.controls['selectedDeletionDatePreset'].value === 0"
>
<input
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
aria-describedby="deletionDateHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</div>
</ng-container>
<div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<input
id="deletionDateCustom"
type="datetime-local"
name="DeletionDate"
aria-describedby="deletionDateHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
</div>
</div>
<div id="deletionDateHelp" class="box-footer">
{{ "deletionDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
isFirefox &&
(editMode ||
(formGroup.controls['selectedDeletionDatePreset'].value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<!-- Expiration Date -->
<div class="box">
<div class="box-content">
<ng-container *ngIf="!editMode">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="editExpirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="ExpirationDateSelect"
aria-describedby="expirationDateHelp"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div
class="box-content-row"
*ngIf="formGroup.controls['selectedExpirationDatePreset'].value === 0"
appBoxRow
>
<input
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
aria-describedby="expirationDateHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disableSend"
/>
</div>
</ng-container>
<div class="box-content-row" *ngIf="editMode" appBoxRow>
<div class="flex-label">
<label>{{ "expirationDate" | i18n }}</label>
<button type="button" *ngIf="!disableSend" appStopClick (click)="clearExpiration()">
{{ "clear" | i18n }}
</button>
</div>
<input
id="expirationDateCustom"
type="datetime-local"
name="ExpirationDate"
aria-describedby="expirationDateHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disableSend"
/>
</div>
</div>
<div id="expirationDateHelp" class="box-footer">
{{ "expirationDateDesc" | i18n }}
<ng-container
*ngIf="
!inPopout &&
isFirefox &&
(editMode ||
(formGroup.controls['selectedExpirationDatePreset'].value === 0 && !editMode))
"
>
<br />{{ "sendFirefoxCustomDatePopoutMessage1" | i18n }}
<a (click)="popOutWindow()">{{ "sendFirefoxCustomDatePopoutMessage2" | i18n }}</a>
{{ "sendFirefoxCustomDatePopoutMessage3" | i18n }}
</ng-container>
</div>
</div>
<!-- Maximum Access Count -->
<div class="box">
<div class="box-content">
@ -190,8 +296,7 @@
type="number"
name="MaximumAccessCount"
aria-describedby="maximumAccessCountHelp"
[(ngModel)]="send.maxAccessCount"
[readonly]="disableSend"
formControlName="maxAccessCount"
/>
</div>
</div>
@ -206,10 +311,9 @@
<label for="currentAccessCount">{{ "currentAccessCount" | i18n }}</label>
<input
id="currentAccessCount"
readonly
type="text"
name="CurrentAccessCount"
[(ngModel)]="send.accessCount"
formControlName="accessCount"
/>
</div>
</div>
@ -227,9 +331,8 @@
name="Password"
aria-describedby="passwordHelp"
class="monospaced"
[(ngModel)]="password"
formControlName="password"
appInputVerbatim
[readonly]="disableSend"
/>
</div>
<div class="action-buttons" *ngIf="!disableSend">
@ -264,8 +367,7 @@
name="Notes"
aria-describedby="notesHelp"
rows="6"
[(ngModel)]="send.notes"
[readonly]="disableSend"
formControlName="notes"
></textarea>
</div>
</div>
@ -278,13 +380,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideEmail">{{ "hideEmail" | i18n }}</label>
<input
id="hideEmail"
type="checkbox"
name="HideEmail"
[(ngModel)]="send.hideEmail"
[disabled]="(disableHideEmail && !send.hideEmail) || disableSend"
/>
<input id="hideEmail" type="checkbox" name="HideEmail" formControlName="hideEmail" />
</div>
</div>
</div>
@ -293,13 +389,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="disableSend">{{ "sendDisableDesc" | i18n }}</label>
<input
id="disableSend"
type="checkbox"
name="DisableSend"
[(ngModel)]="send.disabled"
[disabled]="disableSend"
/>
<input id="disableSend" type="checkbox" name="DisableSend" formControlName="disabled" />
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { DatePipe, Location } from "@angular/common";
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
@ -47,7 +48,8 @@ export class SendAddEditComponent extends BaseAddEditComponent {
private popupUtilsService: PopupUtilsService,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService
dialogService: DialogService,
formBuilder: FormBuilder
) {
super(
i18nService,
@ -60,7 +62,8 @@ export class SendAddEditComponent extends BaseAddEditComponent {
logService,
stateService,
sendApiService,
dialogService
dialogService,
formBuilder
);
}

View File

@ -53,7 +53,6 @@ import { ExportComponent } from "./tools/export/export.component";
import { GeneratorComponent } from "./tools/generator.component";
import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component";
import { AddEditComponent as SendAddEditComponent } from "./tools/send/add-edit.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "./tools/send/efflux-dates.component";
import { SendComponent } from "./tools/send/send.component";
@NgModule({
@ -87,7 +86,6 @@ import { SendComponent } from "./tools/send/send.component";
SearchComponent,
SendAddEditComponent,
SendComponent,
SendEffluxDatesComponent,
SetPasswordComponent,
SetPinComponent,
SettingsComponent,

View File

@ -1,4 +1,4 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<form #form [formGroup]="formGroup" (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<div class="inner-content" *ngIf="send">
<div class="box">
@ -16,14 +16,7 @@
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="send.name"
appAutofocus
[readOnly]="disableSend"
/>
<input id="name" type="text" name="Name" formControlName="name" appAutofocus />
</div>
<div class="box-content-row box-content-row-radio" *ngIf="!editMode">
<label class="radio-header">{{ "whatTypeOfSend" | i18n }}</label>
@ -31,20 +24,16 @@
<input
type="radio"
class="radio"
[(ngModel)]="send.type"
name="Type_{{ o.value }}"
formControlName="type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="send.type === o.value"
[disabled]="disableSend"
/>
<label class="unstyled" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBowRow *ngIf="!editMode && send.type === sendType.File">
<div class="box-content-row" appBoxRow *ngIf="!editMode && type === sendType.File">
<label for="file">{{ "file" | i18n }}</label>
<input
type="file"
@ -53,22 +42,20 @@
name="file"
aria-describedby="fileHelp"
required
[disabled]="disableSend"
/>
</div>
<div class="box-content-row" appBowRow *ngIf="editMode && send.type === sendType.File">
<div class="box-content-row" appBoxRow *ngIf="editMode && type === sendType.File">
<label for="file">{{ "file" | i18n }}</label>
<div class="row-main">{{ send.file.fileName }} ({{ send.file.sizeName }})</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="send.type === sendType.Text">
<div class="box-content-row" appBoxRow *ngIf="type === sendType.Text">
<label for="text">{{ "text" | i18n }}</label>
<textarea
id="text"
name="text"
aria-describedby="textHelp"
[(ngModel)]="send.text.text"
formControlName="text"
rows="6"
[readOnly]="disableSend"
></textarea>
</div>
</div>
@ -83,13 +70,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideText">{{ "textHiddenByDefault" | i18n }}</label>
<input
id="hideText"
name="hideText"
type="checkbox"
[(ngModel)]="send.text.hidden"
[disabled]="disableSend"
/>
<input id="hideText" name="hideText" type="checkbox" formControlName="textHidden" />
</div>
</div>
</div>
@ -112,14 +93,82 @@
</h2>
</div>
<div [hidden]="!showOptions">
<app-send-efflux-dates
[initialDeletionDate]="send.deletionDate"
[initialExpirationDate]="send.expirationDate"
[editMode]="editMode"
[disabled]="disableSend"
(datesChanged)="setDates($event)"
>
</app-send-efflux-dates>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
aria-describedby="deletionDateHelp"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
<small id="deletionDateHelp" class="help-block">{{
"deletionDateDesc" | i18n
}}</small>
</div>
<div
class="box-content-row"
*ngIf="formGroup.controls['selectedDeletionDatePreset'].value === 0 || editMode"
>
<label *ngIf="editMode" for="deletionDateCustom">{{ "deletionDate" | i18n }}</label>
<input
id="deletionDateCustom"
type="datetime-local"
name="deletionDate"
aria-describedby="deletionDateCustomHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<small id="deletionDateCustomHelp" class="help-block" *ngIf="editMode">{{
"deletionDateDesc" | i18n
}}</small>
</div>
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="expirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="expirationDateSelect"
aria-describedby="expirationDateHelp"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
<small id="expirationDateHelp" class="help-block">{{
"expirationDateDesc" | i18n
}}</small>
</div>
<div
class="box-content-row"
*ngIf="formGroup.controls['selectedExpirationDatePreset'].value === 0 || editMode"
>
<label *ngIf="editMode" for="expirationDateCustom">{{
"expirationDate" | i18n
}}</label>
<input
id="expirationDateCustom"
type="datetime-local"
name="expirationDate"
aria-describedby="expirationDateCustomHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<small *ngIf="editMode" id="expirationDateCustomHelp" class="help-block">{{
"expirationDateDesc" | i18n
}}</small>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
@ -129,8 +178,7 @@
type="number"
name="maxAccessCount"
aria-describedby="maxAccessCountHelp"
[(ngModel)]="send.maxAccessCount"
[readOnly]="disableSend"
formControlName="maxAccessCount"
/>
</div>
</div>
@ -154,8 +202,7 @@
name="password"
aria-describedby="passwordHelp"
type="{{ showPassword ? 'text' : 'password' }}"
[(ngModel)]="password"
[readOnly]="disableSend"
formControlName="password"
appInputVerbatim
/>
</div>
@ -167,7 +214,6 @@
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePasswordVisible()"
[disabled]="disableSend"
>
<i
class="bwi bwi-lg"
@ -192,9 +238,8 @@
id="notes"
name="notes"
aria-describedby="notesHelp"
[(ngModel)]="send.notes"
formControlName="notes"
rows="6"
[readOnly]="disableSend"
></textarea>
</div>
</div>
@ -206,13 +251,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideEmail">{{ "hideEmail" | i18n }}</label>
<input
id="hideEmail"
type="checkbox"
name="HideEmail"
[(ngModel)]="send.hideEmail"
[disabled]="(disableHideEmail && !send.hideEmail) || disableSend"
/>
<input id="hideEmail" type="checkbox" name="HideEmail" formControlName="hideEmail" />
</div>
</div>
</div>
@ -220,13 +259,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="disabled">{{ "disableSend" | i18n }}</label>
<input
id="disabled"
type="checkbox"
name="disabled"
[(ngModel)]="send.disabled"
[disabled]="disableSend"
/>
<input id="disabled" type="checkbox" name="disabled" formControlName="disabled" />
</div>
</div>
</div>
@ -238,17 +271,11 @@
<div class="box-content">
<div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="link">{{ "sendLinkLabel" | i18n }}</label>
<input id="link" name="link" [ngModel]="link" readonly />
<input id="link" name="link" formControlName="link" readonly />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="copyLink">{{ "copySendLinkOnSave" | i18n }}</label>
<input
id="copyLink"
name="copyLink"
[(ngModel)]="copyLink"
type="checkbox"
[disabled]="disableSend"
/>
<input id="copyLink" name="copyLink" formControlName="copyLink" type="checkbox" />
</div>
</div>
</div>
@ -259,13 +286,12 @@
type="submit"
class="primary btn-submit"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
*ngIf="!disableSend"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span><i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i></span>
</button>
<button type="button" (click)="cancel()" [disabled]="form.loading">
<button type="button" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
<div class="right">

View File

@ -1,5 +1,6 @@
import { DatePipe } from "@angular/common";
import { Component } 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";
@ -29,7 +30,8 @@ export class AddEditComponent extends BaseAddEditComponent {
policyService: PolicyService,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService
dialogService: DialogService,
formBuilder: FormBuilder
) {
super(
i18nService,
@ -42,7 +44,8 @@ export class AddEditComponent extends BaseAddEditComponent {
logService,
stateService,
sendApiService,
dialogService
dialogService,
formBuilder
);
}
@ -50,6 +53,7 @@ export class AddEditComponent extends BaseAddEditComponent {
this.password = null;
const send = await this.loadSend();
this.send = await send.decrypt();
this.updateFormValues();
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
}
@ -65,4 +69,11 @@ export class AddEditComponent extends BaseAddEditComponent {
this.i18nService.t("valueCopied", this.i18nService.t("sendLink"))
);
}
async resetAndLoad() {
this.sendId = null;
this.send = null;
await this.load();
this.updateFormValues();
}
}

View File

@ -1,62 +0,0 @@
<ng-container [formGroup]="datesForm">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
aria-describedby="deletionDateHelp"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small id="deletionDateHelp" class="help-block">{{ "deletionDateDesc" | i18n }}</small>
</div>
<div class="box-content-row" *ngIf="selectedDeletionDatePreset.value === 0 || editMode">
<label *ngIf="editMode" for="deletionDateCustom">{{ "deletionDate" | i18n }}</label>
<input
id="deletionDateCustom"
type="datetime-local"
name="deletionDate"
aria-describedby="deletionDateCustomHelp"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<small id="deletionDateCustomHelp" class="help-block" *ngIf="editMode">{{
"deletionDateDesc" | i18n
}}</small>
</div>
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="expirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="expirationDateSelect"
aria-describedby="expirationDateHelp"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small id="expirationDateHelp" class="help-block">{{ "expirationDateDesc" | i18n }}</small>
</div>
<div class="box-content-row" *ngIf="selectedExpirationDatePreset.value === 0 || editMode">
<label *ngIf="editMode" for="expirationDateCustom">{{ "expirationDate" | i18n }}</label>
<input
id="expirationDateCustom"
type="datetime-local"
name="expirationDate"
aria-describedby="expirationDateCustomHelp"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<small *ngIf="editMode" id="expirationDateCustomHelp" class="help-block">{{
"expirationDateDesc" | i18n
}}</small>
</div>
</div>
</div>
</ng-container>

View File

@ -1,38 +0,0 @@
import { DatePipe } from "@angular/common";
import { Component, OnChanges } from "@angular/core";
import { ControlContainer, NgForm } from "@angular/forms";
import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({
selector: "app-send-efflux-dates",
templateUrl: "efflux-dates.component.html",
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class EffluxDatesComponent extends BaseEffluxDatesComponent implements OnChanges {
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {
super(i18nService, platformUtilsService, datePipe);
}
// We reuse the same form on desktop and just swap content, so need to watch these to maintin proper values.
ngOnChanges() {
this.selectedExpirationDatePreset.setValue(0);
this.selectedDeletionDatePreset.setValue(0);
this.defaultDeletionDateTime.setValue(
this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm")
);
if (this.initialExpirationDate) {
this.defaultExpirationDateTime.setValue(
this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm")
);
} else {
this.defaultExpirationDateTime.setValue(null);
}
}
}

View File

@ -91,12 +91,10 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
this.searchBarService.setEnabled(false);
}
addSend() {
async addSend() {
this.action = Action.Add;
if (this.addEditComponent != null) {
this.addEditComponent.sendId = null;
this.addEditComponent.send = null;
this.addEditComponent.load();
await this.addEditComponent.resetAndLoad();
}
}

View File

@ -93,7 +93,6 @@ import { GeneratorComponent } from "../tools/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
import { AccessComponent } from "../tools/send/access.component";
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "../tools/send/efflux-dates.component";
import { ToolsComponent } from "../tools/tools.component";
import { PasswordRepromptComponent } from "../vault/components/password-reprompt.component";
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
@ -198,7 +197,6 @@ import { SharedModule } from "./shared.module";
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SendEffluxDatesComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
@ -302,7 +300,6 @@ import { SharedModule } from "./shared.module";
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SendEffluxDatesComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,

View File

@ -1,303 +1,276 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="modal-header">
<h1 class="modal-title" id="sendAddEditTitle">{{ title }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
<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"
>
<bit-label>{{ o.name }}</bit-label>
</bit-radio-button>
</bit-radio-group>
</div>
<div class="modal-body" *ngIf="send">
<app-callout *ngIf="disableSend">
<span>{{ "sendDisabledWarning" | i18n }}</span>
</app-callout>
<app-callout *ngIf="!disableSend && disableHideEmail">
<span>{{ "sendOptionsPolicyInEffect" | i18n }}</span>
<ul class="mb-0">
<li>{{ "sendDisableHideEmailInEffect" | i18n }}</li>
</ul>
</app-callout>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="send.name"
required
[readOnly]="disableSend"
/>
<small class="form-text text-muted">{{ "sendNameDesc" | i18n }}</small>
</div>
</div>
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label>{{ "whatTypeOfSend" | i18n }}</label>
<div class="form-check" *ngFor="let o of typeOptions">
<input
class="form-check-input"
type="radio"
[(ngModel)]="send.type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="send.type === o.value"
/>
<label class="form-check-label" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{ "sendTypeText" | i18n }}</label>
<textarea
id="text"
name="Text.Text"
rows="6"
[(ngModel)]="send.text.text"
class="form-control"
[readOnly]="disableSend"
></textarea>
<small class="form-text text-muted">{{ "sendTextDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.text.hidden"
id="text-hidden"
name="Text.Hidden"
[disabled]="disableSend"
/>
<label class="form-check-label" for="text-hidden">{{
"textHiddenByDefault" | i18n
}}</label>
</div>
</div>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<div class="form-group">
<div *ngIf="editMode">
<strong class="d-block">{{ "file" | i18n }}</strong>
<!-- 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 }})
</div>
<div *ngIf="!editMode">
<label for="file">{{ "file" | i18n }}</label>
<input
type="file"
id="file"
class="form-control-file"
name="file"
required
[disabled]="disableSend"
/>
<small class="form-text text-muted"
>{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}</small
>
</div>
</p>
</div>
</ng-container>
<h3 class="mt-5">{{ "share" | i18n }}</h3>
<div class="form-group" *ngIf="link">
<label for="link">{{ "sendLinkLabel" | i18n }}</label>
<input type="text" readonly id="link" name="Link" [ngModel]="link" class="form-control" />
</div>
<div class="form-group">
<div class="form-check">
<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
class="form-check-input"
type="checkbox"
[(ngModel)]="copyLink"
id="copy-link"
name="CopyLink"
bitInput
#fileSelector
hidden
type="file"
id="file"
name="file"
formControlName="file"
(change)="setSelectedFile($event)"
/>
<label class="form-check-label" for="copy-link">{{
"copySendLinkOnSave" | i18n
}}</label>
</div>
<bit-hint>{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div
id="options-header"
class="section-header d-flex flex-row align-items-center mt-5"
(click)="toggleOptions()"
>
<h3 class="mb-0 mr-2">
<button type="button" appStopClick class="header-expandable">
<i
class="bwi"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</h3>
</div>
<div id="options" [hidden]="!showOptions">
<app-send-efflux-dates
[initialDeletionDate]="send.deletionDate"
[initialExpirationDate]="send.expirationDate"
[editMode]="editMode"
[disabled]="disableSend"
(datesChanged)="setDates($event)"
>
</app-send-efflux-dates>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{ "maxAccessCount" | i18n }}</label>
<input
id="maxAccessCount"
class="form-control"
type="number"
name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount"
min="1"
[readOnly]="disableSend"
/>
<div class="form-text text-muted small">{{ "maxAccessCountDesc" | i18n }}</div>
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{ "currentAccessCount" | i18n }}</label>
<input
id="accessCount"
class="form-control"
type="text"
name="AccessCount"
readonly
[(ngModel)]="send.accessCount"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{ "password" | i18n }}</label>
<label for="password" *ngIf="hasPassword">{{ "newPassword" | i18n }}</label>
<div class="input-group">
<input
id="password"
class="form-control text-monospace"
type="{{ showPassword ? 'text' : 'password' }}"
name="Password"
[(ngModel)]="password"
[readOnly]="disableSend"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePasswordVisible()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-text text-muted small">{{ "sendPasswordDesc" | i18n }}</div>
</div>
</div>
<div class="form-group">
<label for="notes">{{ "notes" | i18n }}</label>
<textarea
id="notes"
name="Notes"
rows="6"
[(ngModel)]="send.notes"
class="form-control"
[readOnly]="disableSend"
></textarea>
<div class="form-text text-muted small">{{ "sendNotesDesc" | i18n }}</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.hideEmail"
id="hideEmail"
name="HideEmail"
[disabled]="(disableHideEmail && !send.hideEmail) || disableSend"
/>
<label class="form-check-label" for="hideEmail">
{{ "hideEmail" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="send.disabled"
id="disabled"
name="Disabled"
[disabled]="disableSend"
/>
<label class="form-check-label" for="disabled">{{ "disableThisSend" | i18n }}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[ngClass]="{ loading: form.loading }"
[disabled]="form.loading || disableSend"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto" *ngIf="send">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>
</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>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
class="bwi"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</div>
</h4>
</div>
</form>
</div>
</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"
>
{{ "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,5 +1,7 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
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";
@ -19,6 +21,7 @@ import { DialogService } from "@bitwarden/components";
})
export class AddEditComponent extends BaseAddEditComponent {
override componentName = "app-send-add-edit";
protected selectedFile: File;
constructor(
i18nService: I18nService,
@ -31,7 +34,10 @@ export class AddEditComponent extends BaseAddEditComponent {
policyService: PolicyService,
logService: LogService,
sendApiService: SendApiService,
dialogService: DialogService
dialogService: DialogService,
formBuilder: FormBuilder,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: { sendId: string }
) {
super(
i18nService,
@ -44,8 +50,11 @@ export class AddEditComponent extends BaseAddEditComponent {
logService,
stateService,
sendApiService,
dialogService
dialogService,
formBuilder
);
this.sendId = params.sendId;
}
async copyLinkToClipboard(link: string): Promise<void | boolean> {
@ -55,4 +64,29 @@ export class AddEditComponent extends BaseAddEditComponent {
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

@ -1,188 +0,0 @@
<div class="row" [formGroup]="datesForm">
<div class="col-6 form-group">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<ng-template #deletionDateCustom>
<ng-container [ngSwitch]="browserPath">
<ng-container *ngSwitchCase="'firefox'">
<div class="d-flex justify-content-around">
<input
id="deletionDateCustomFallback"
class="form-control mt-1"
type="date"
name="DeletionDateFallback"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
data-date-format="mm/dd/yyyy"
/>
<input
id="deletionTimeCustomFallback"
class="form-control mt-1 ml-1"
type="time"
name="DeletionTimeDate"
formControlName="fallbackDeletionTime"
required
placeholder="HH:MM AM/PM"
/>
</div>
</ng-container>
<ng-container *ngSwitchCase="'safari'">
<div class="d-flex justify-content-around">
<input
id="deletionDateCustomFallback"
class="form-control mt-1"
type="date"
name="DeletionDateFallback"
formControlName="fallbackDeletionDate"
required
placeholder="MM/DD/YYYY"
data-date-format="mm/dd/yyyy"
/>
<select
id="deletionTimeCustomFallback"
class="form-control mt-1 ml-1"
[required]="!editMode"
formControlName="fallbackDeletionTime"
name="SafariDeletionTime"
>
<option
*ngFor="let o of safariDeletionTimePresetOptions"
[ngValue]="o.twentyFourHour"
>
{{ o.twelveHour }}
</option>
</select>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
<input
id="deletionDateCustom"
class="form-control mt-1"
type="datetime-local"
name="DeletionDate"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disabled"
/>
</ng-container>
</ng-container>
</ng-template>
<div *ngIf="!editMode">
<select
id="deletionDate"
name="SelectedDeletionDatePreset"
formControlName="selectedDeletionDatePreset"
class="form-control"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="selectedDeletionDatePreset.value === 0">
<ng-container *ngTemplateOutlet="deletionDateCustom"> </ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="deletionDateCustom"> </ng-container>
</div>
<div class="form-text text-muted small">{{ "deletionDateDesc" | i18n }}</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{ "expirationDate" | i18n }}</label>
<a
href="#"
appStopClick
(click)="clearExpiration()"
class="ml-auto"
*ngIf="editMode && !disabled"
>
{{ "clear" | i18n }}
</a>
</div>
<ng-template #expirationDateCustom>
<ng-container [ngSwitch]="browserPath">
<div *ngSwitchCase="'firefox'" class="d-flex justify-content-around">
<input
id="expirationDateCustomFallback"
class="form-control mt-1"
type="date"
name="ExpirationDateFallback"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<input
id="expirationTimeCustomFallback"
class="form-control mt-1 ml-1"
type="time"
name="ExpirationTimeFallback"
formControlName="fallbackExpirationTime"
[required]="!editMode"
placeholder="HH:MM AM/PM"
[readOnly]="disabled"
/>
</div>
<!-- non-default cases are not showing up -->
<div *ngSwitchCase="'safari'" class="d-flex justify-content-around">
<input
id="expirationDateCustomFallback"
class="form-control mt-1"
type="date"
name="ExpirationDateFallback"
formControlName="fallbackExpirationDate"
[required]="!editMode"
placeholder="MM/DD/YYYY"
[readOnly]="disabled"
data-date-format="mm/dd/yyyy"
/>
<select
id="expirationTimeCustomFallback"
class="form-control mt-1 ml-1"
[required]="!editMode"
formControlName="fallbackExpirationTime"
name="SafariExpirationTime"
>
<option
*ngFor="let o of safariExpirationTimePresetOptions"
[ngValue]="o.twentyFourHour"
>
{{ o.twelveHour }}
</option>
</select>
</div>
<ng-container *ngSwitchDefault>
<input
id="expirationDateCustom"
class="form-control mt-1"
type="datetime-local"
name="ExpirationDate"
formControlName="defaultExpirationDateTime"
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disabled"
/>
</ng-container>
</ng-container>
</ng-template>
<div *ngIf="!editMode">
<select
id="expirationDate"
name="SelectedExpirationDatePreset"
formControlName="selectedExpirationDatePreset"
class="form-control"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="selectedExpirationDatePreset.value === 0">
<ng-container *ngTemplateOutlet="expirationDateCustom"> </ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="expirationDateCustom"> </ng-container>
</div>
<div class="form-text text-muted small">{{ "expirationDateDesc" | i18n }}</div>
</div>
</div>

View File

@ -1,22 +0,0 @@
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
import { ControlContainer, NgForm } from "@angular/forms";
import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({
selector: "app-send-efflux-dates",
templateUrl: "efflux-dates.component.html",
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class EffluxDatesComponent extends BaseEffluxDatesComponent {
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {
super(i18nService, platformUtilsService, datePipe);
}
}

View File

@ -1,4 +1,5 @@
import { Component, NgZone, ViewChild, ViewContainerRef } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
@ -98,29 +99,17 @@ export class SendComponent extends BaseSendComponent {
return;
}
const component = await this.editSend(null);
component.type = this.type;
await this.editSend(null);
}
async editSend(send: SendView) {
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.sendAddEditModalRef,
(comp) => {
comp.sendId = send == null ? null : send.id;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onSavedSend.subscribe(async () => {
modal.close();
await this.load();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onDeletedSend.subscribe(async () => {
modal.close();
await this.load();
});
}
);
const dialog = this.dialogService.open(AddEditComponent, {
data: {
sendId: send == null ? null : send.id,
},
});
return childComponent;
await lastValueFrom(dialog.closed);
await this.load();
}
}

View File

@ -1,5 +1,6 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -20,6 +21,23 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService } from "@bitwarden/components";
// Value = hours
enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
ThirtyDays = 720,
Custom = 0,
Never = null,
}
interface DatePresetSelectOption {
name: string;
value: DatePreset;
}
@Directive()
export class AddEditComponent implements OnInit, OnDestroy {
@Input() sendId: string;
@ -29,12 +47,25 @@ export class AddEditComponent implements OnInit, OnDestroy {
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
deletionDatePresets: DatePresetSelectOption[] = [
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
{ name: this.i18nService.t("custom"), value: DatePreset.Custom },
];
expirationDatePresets: DatePresetSelectOption[] = [
{ name: this.i18nService.t("never"), value: DatePreset.Never },
...this.deletionDatePresets,
];
copyLink = false;
disableSend = false;
disableHideEmail = false;
send: SendView;
deletionDate: string;
expirationDate: string;
hasPassword: boolean;
password: string;
showPassword = false;
@ -51,6 +82,27 @@ export class AddEditComponent implements OnInit, OnDestroy {
private sendLinkBaseUrl: string;
private destroy$ = new Subject<void>();
protected formGroup = this.formBuilder.group({
name: ["", Validators.required],
text: [],
textHidden: [false],
fileContents: [],
file: [null, Validators.required],
link: [],
copyLink: false,
maxAccessCount: [],
accessCount: [],
password: [],
notes: [],
hideEmail: false,
disabled: false,
type: [],
defaultExpirationDateTime: [],
defaultDeletionDateTime: ["", Validators.required],
selectedDeletionDatePreset: [DatePreset.SevenDays, Validators.required],
selectedExpirationDatePreset: [],
});
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
@ -59,10 +111,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected sendService: SendService,
protected messagingService: MessagingService,
protected policyService: PolicyService,
private logService: LogService,
protected logService: LogService,
protected stateService: StateService,
protected sendApiService: SendApiService,
protected dialogService: DialogService
protected dialogService: DialogService,
protected formBuilder: FormBuilder
) {
this.typeOptions = [
{ name: i18nService.t("sendTypeFile"), value: SendType.File },
@ -72,7 +125,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
get link(): string {
if (this.send.id != null && this.send.accessId != null) {
if (this.send != null && this.send.id != null && this.send.accessId != null) {
return this.sendLinkBaseUrl + this.send.accessId + "/" + this.send.urlB64Key;
}
return null;
@ -92,13 +145,39 @@ export class AddEditComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.disableSend = policyAppliesToActiveUser;
if (this.disableSend) {
this.formGroup.disable();
}
});
this.policyService
.policyAppliesToActiveUser$(PolicyType.SendOptions, (p) => p.data.disableHideEmail)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.disableHideEmail = policyAppliesToActiveUser;
if ((this.disableHideEmail = policyAppliesToActiveUser)) {
this.formGroup.controls.hideEmail.disable();
}
});
this.formGroup.controls.type.valueChanges.subscribe((val) => {
this.type = val;
this.typeChanged();
});
this.formGroup.controls.selectedDeletionDatePreset.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((datePreset) => {
datePreset === DatePreset.Custom
? this.formGroup.controls.defaultDeletionDateTime.enable()
: this.formGroup.controls.defaultDeletionDateTime.disable();
});
this.formGroup.controls.hideEmail.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((val) => {
if (!val && this.disableHideEmail) {
this.formGroup.controls.hideEmail.disable();
}
});
await this.load();
@ -117,29 +196,33 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.i18nService.t(this.editMode ? "editSend" : "createSend");
}
setDates(event: { deletionDate: string; expirationDate: string }) {
this.deletionDate = event.deletionDate;
this.expirationDate = event.expirationDate;
}
async load() {
this.canAccessPremium = await this.stateService.getCanAccessPremium();
this.emailVerified = await this.stateService.getEmailVerified();
if (!this.canAccessPremium || !this.emailVerified) {
this.type = SendType.Text;
}
this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File;
if (this.send == null) {
if (this.editMode) {
const send = this.loadSend();
this.send = await send.decrypt();
this.type = this.send.type;
this.updateFormValues();
if (this.send.hideEmail) {
this.formGroup.controls.hideEmail.enable();
}
} else {
this.send = new SendView();
this.send.type = this.type == null ? SendType.File : this.type;
this.send.type = this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
this.formGroup.controls.type.patchValue(this.send.type);
this.formGroup.patchValue({
selectedDeletionDatePreset: DatePreset.SevenDays,
selectedExpirationDatePreset: DatePreset.Never,
});
}
}
@ -147,6 +230,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
async submit(): Promise<boolean> {
this.formGroup.markAllAsTouched();
if (this.disableSend) {
this.platformUtilsService.showToast(
"error",
@ -156,6 +241,17 @@ export class AddEditComponent implements OnInit, OnDestroy {
return false;
}
this.send.name = this.formGroup.controls.name.value;
this.send.text.text = this.formGroup.controls.text.value;
this.send.text.hidden = this.formGroup.controls.textHidden.value;
this.send.maxAccessCount = this.formGroup.controls.maxAccessCount.value;
this.send.accessCount = this.formGroup.controls.accessCount.value;
this.send.password = this.formGroup.controls.password.value;
this.send.notes = this.formGroup.controls.notes.value;
this.send.hideEmail = this.formGroup.controls.hideEmail.value;
this.send.disabled = this.formGroup.controls.disabled.value;
this.send.type = this.type;
if (this.send.name == null || this.send.name === "") {
this.platformUtilsService.showToast(
"error",
@ -166,7 +262,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
let file: File = null;
if (this.send.type === SendType.File && !this.editMode) {
if (this.type === SendType.File && !this.editMode) {
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
@ -190,7 +286,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
if (this.password != null && this.password.trim() === "") {
if (
this.formGroup.controls.password.value != null &&
this.formGroup.controls.password.value.trim() === ""
) {
this.password = null;
}
@ -204,7 +303,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.send.accessId = encSend[0].accessId;
}
this.onSavedSend.emit(this.send);
if (this.copyLink && this.link != null) {
if (this.formGroup.controls.copyLink.value && this.link != null) {
await this.handleCopyLinkToClipboard();
return;
}
@ -227,7 +326,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
return Promise.resolve(this.platformUtilsService.copyToClipboard(link));
}
async delete(): Promise<boolean> {
protected async delete(): Promise<boolean> {
if (this.deletePromise != null) {
return false;
}
@ -257,7 +356,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
typeChanged() {
if (this.send.type === SendType.File && !this.alertShown) {
if (this.type === SendType.File && !this.alertShown) {
if (!this.canAccessPremium) {
this.alertShown = true;
this.messagingService.send("premiumRequired");
@ -266,6 +365,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.messagingService.send("emailVerificationRequired");
}
}
this.type === SendType.Text || this.editMode
? this.formGroup.controls.file.disable()
: this.formGroup.controls.file.enable();
}
toggleOptions() {
@ -277,17 +379,23 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> {
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
const sendData = await this.sendService.encrypt(
this.send,
file,
this.formGroup.controls.password.value,
null
);
// Parse dates
try {
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
sendData[0].deletionDate =
this.formattedDeletionDate == null ? null : new Date(this.formattedDeletionDate);
} catch {
sendData[0].deletionDate = null;
}
try {
sendData[0].expirationDate =
this.expirationDate == null ? null : new Date(this.expirationDate);
this.formattedExpirationDate == null ? null : new Date(this.formattedExpirationDate);
} catch {
sendData[0].expirationDate = null;
}
@ -299,6 +407,34 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showPassword = !this.showPassword;
document.getElementById("password").focus();
}
updateFormValues() {
this.formGroup.patchValue({
name: this.send?.name ?? "",
text: this.send?.text?.text ?? "",
textHidden: this.send?.text?.hidden ?? false,
link: this.link ?? "",
maxAccessCount: this.send?.maxAccessCount,
accessCount: this.send?.accessCount ?? 0,
notes: this.send?.notes ?? "",
hideEmail: this.send?.hideEmail ?? false,
disabled: this.send?.disabled ?? false,
type: this.send.type ?? this.type,
password: "",
selectedDeletionDatePreset: this.editMode ? DatePreset.Custom : DatePreset.SevenDays,
selectedExpirationDatePreset: this.editMode ? DatePreset.Custom : DatePreset.Never,
defaultExpirationDateTime:
this.send.expirationDate != null
? this.datePipe.transform(new Date(this.send.expirationDate), "yyyy-MM-ddTHH:mm")
: null,
defaultDeletionDateTime: this.datePipe.transform(
new Date(this.send.deletionDate),
"yyyy-MM-ddTHH:mm"
),
});
}
private async handleCopyLinkToClipboard() {
const copySuccess = await this.copyLinkToClipboard(this.link);
if (copySuccess ?? true) {
@ -319,4 +455,46 @@ export class AddEditComponent implements OnInit, OnDestroy {
await this.copyLinkToClipboard(this.link);
}
}
clearExpiration() {
this.formGroup.controls.defaultExpirationDateTime.patchValue(null);
}
get formattedExpirationDate(): string {
switch (this.formGroup.controls.selectedExpirationDatePreset.value as DatePreset) {
case DatePreset.Never:
return null;
case DatePreset.Custom:
if (!this.formGroup.controls.defaultExpirationDateTime.value) {
return null;
}
return this.formGroup.controls.defaultExpirationDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
get formattedDeletionDate(): string {
switch (this.formGroup.controls.selectedDeletionDatePreset.value as DatePreset) {
case DatePreset.Never:
this.formGroup.controls.selectedDeletionDatePreset.patchValue(DatePreset.SevenDays);
return this.formattedDeletionDate;
case DatePreset.Custom:
return this.formGroup.controls.defaultDeletionDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
}

View File

@ -1,356 +0,0 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
// Different BrowserPath = different controls.
enum BrowserPath {
// Native datetime-locale.
// We are happy.
Default = "default",
// Native date and time inputs, but no datetime-locale.
// We use individual date and time inputs and create a datetime programatically on submit.
Firefox = "firefox",
// No native date, time, or datetime-locale inputs.
// We use a polyfill for dates and a dropdown for times.
Safari = "safari",
}
enum DateField {
DeletionDate = "deletion",
ExpirationDate = "expiration",
}
// Value = hours
enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
ThirtyDays = 720,
Custom = 0,
Never = null,
}
// TimeOption is used for the dropdown implementation of custom times
// twelveHour = displayed time; twentyFourHour = time used in logic
interface TimeOption {
twelveHour: string;
twentyFourHour: string;
}
@Directive()
export class EffluxDatesComponent implements OnInit {
@Input() readonly initialDeletionDate: Date;
@Input() readonly initialExpirationDate: Date;
@Input() readonly editMode: boolean;
@Input() readonly disabled: boolean;
@Output() datesChanged = new EventEmitter<{ deletionDate: string; expirationDate: string }>();
get browserPath(): BrowserPath {
if (this.platformUtilsService.isFirefox()) {
return BrowserPath.Firefox;
} else if (this.platformUtilsService.isSafari()) {
return BrowserPath.Safari;
}
return BrowserPath.Default;
}
datesForm = new UntypedFormGroup({
selectedDeletionDatePreset: new UntypedFormControl(),
selectedExpirationDatePreset: new UntypedFormControl(),
defaultDeletionDateTime: new UntypedFormControl(),
defaultExpirationDateTime: new UntypedFormControl(),
fallbackDeletionDate: new UntypedFormControl(),
fallbackDeletionTime: new UntypedFormControl(),
fallbackExpirationDate: new UntypedFormControl(),
fallbackExpirationTime: new UntypedFormControl(),
});
deletionDatePresets: any[] = [
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
{ name: this.i18nService.t("custom"), value: DatePreset.Custom },
];
expirationDatePresets: any[] = [
{ name: this.i18nService.t("never"), value: DatePreset.Never },
].concat([...this.deletionDatePresets]);
get selectedDeletionDatePreset(): UntypedFormControl {
return this.datesForm.get("selectedDeletionDatePreset") as UntypedFormControl;
}
get selectedExpirationDatePreset(): UntypedFormControl {
return this.datesForm.get("selectedExpirationDatePreset") as UntypedFormControl;
}
get defaultDeletionDateTime(): UntypedFormControl {
return this.datesForm.get("defaultDeletionDateTime") as UntypedFormControl;
}
get defaultExpirationDateTime(): UntypedFormControl {
return this.datesForm.get("defaultExpirationDateTime") as UntypedFormControl;
}
get fallbackDeletionDate(): UntypedFormControl {
return this.datesForm.get("fallbackDeletionDate") as UntypedFormControl;
}
get fallbackDeletionTime(): UntypedFormControl {
return this.datesForm.get("fallbackDeletionTime") as UntypedFormControl;
}
get fallbackExpirationDate(): UntypedFormControl {
return this.datesForm.get("fallbackExpirationDate") as UntypedFormControl;
}
get fallbackExpirationTime(): UntypedFormControl {
return this.datesForm.get("fallbackExpirationTime") as UntypedFormControl;
}
// Should be able to call these at any time and compute a submitable value
get formattedDeletionDate(): string {
switch (this.selectedDeletionDatePreset.value as DatePreset) {
case DatePreset.Never:
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
return this.formattedDeletionDate;
case DatePreset.Custom:
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
return this.fallbackDeletionDate.value + "T" + this.fallbackDeletionTime.value;
default:
return this.defaultDeletionDateTime.value;
}
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() + (this.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
get formattedExpirationDate(): string {
switch (this.selectedExpirationDatePreset.value as DatePreset) {
case DatePreset.Never:
return null;
case DatePreset.Custom:
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
if (
(!this.fallbackExpirationDate.value || !this.fallbackExpirationTime.value) &&
this.editMode
) {
return null;
}
return this.fallbackExpirationDate.value + "T" + this.fallbackExpirationTime.value;
default:
if (!this.defaultExpirationDateTime.value) {
return null;
}
return this.defaultExpirationDateTime.value;
}
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() + (this.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000
);
return new Date(milliseconds).toString();
}
}
}
//
get safariDeletionTimePresetOptions() {
return this.safariTimePresetOptions(DateField.DeletionDate);
}
get safariExpirationTimePresetOptions() {
return this.safariTimePresetOptions(DateField.ExpirationDate);
}
private get nextWeek(): Date {
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek;
}
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {}
ngOnInit(): void {
this.setInitialFormValues();
this.emitDates();
this.datesForm.valueChanges.subscribe(() => {
this.emitDates();
});
}
onDeletionDatePresetSelect(value: DatePreset) {
this.selectedDeletionDatePreset.setValue(value);
}
clearExpiration() {
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
this.fallbackExpirationDate.setValue(null);
this.fallbackExpirationTime.setValue(null);
break;
case BrowserPath.Default:
this.defaultExpirationDateTime.setValue(null);
break;
}
}
protected emitDates() {
this.datesChanged.emit({
deletionDate: this.formattedDeletionDate,
expirationDate: this.formattedExpirationDate,
});
}
protected setInitialFormValues() {
if (this.editMode) {
this.selectedDeletionDatePreset.setValue(DatePreset.Custom);
this.selectedExpirationDatePreset.setValue(DatePreset.Custom);
switch (this.browserPath) {
case BrowserPath.Safari:
case BrowserPath.Firefox:
this.fallbackDeletionDate.setValue(this.initialDeletionDate.toISOString().slice(0, 10));
this.fallbackDeletionTime.setValue(this.initialDeletionDate.toTimeString().slice(0, 5));
if (this.initialExpirationDate != null) {
this.fallbackExpirationDate.setValue(
this.initialExpirationDate.toISOString().slice(0, 10)
);
this.fallbackExpirationTime.setValue(
this.initialExpirationDate.toTimeString().slice(0, 5)
);
}
break;
case BrowserPath.Default:
if (this.initialExpirationDate) {
this.defaultExpirationDateTime.setValue(
this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm")
);
}
this.defaultDeletionDateTime.setValue(
this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm")
);
break;
}
} else {
this.selectedDeletionDatePreset.setValue(DatePreset.SevenDays);
this.selectedExpirationDatePreset.setValue(DatePreset.Never);
switch (this.browserPath) {
case BrowserPath.Safari:
this.fallbackDeletionDate.setValue(this.nextWeek.toISOString().slice(0, 10));
this.fallbackDeletionTime.setValue(
this.safariTimePresetOptions(DateField.DeletionDate)[1].twentyFourHour
);
break;
default:
break;
}
}
}
protected safariTimePresetOptions(field: DateField): TimeOption[] {
// init individual arrays for major sort groups
const noon: TimeOption[] = [];
const midnight: TimeOption[] = [];
const ams: TimeOption[] = [];
const pms: TimeOption[] = [];
// determine minute skip (5 min, 10 min, 15 min, etc.)
const minuteIncrementer = 15;
// loop through each hour on a 12 hour system
for (let h = 1; h <= 12; h++) {
// loop through each minute in the hour using the skip to increment
for (let m = 0; m < 60; m += minuteIncrementer) {
// init the final strings that will be added to the lists
let hour = h.toString();
let minutes = m.toString();
// add prepending 0s to single digit hours/minutes
if (h < 10) {
hour = "0" + hour;
}
if (m < 10) {
minutes = "0" + minutes;
}
// build time strings and push to relevant sort groups
if (h === 12) {
const midnightOption: TimeOption = {
twelveHour: `${hour}:${minutes} AM`,
twentyFourHour: `00:${minutes}`,
};
midnight.push(midnightOption);
const noonOption: TimeOption = {
twelveHour: `${hour}:${minutes} PM`,
twentyFourHour: `${hour}:${minutes}`,
};
noon.push(noonOption);
} else {
const amOption: TimeOption = {
twelveHour: `${hour}:${minutes} AM`,
twentyFourHour: `${hour}:${minutes}`,
};
ams.push(amOption);
const pmOption: TimeOption = {
twelveHour: `${hour}:${minutes} PM`,
twentyFourHour: `${h + 12}:${minutes}`,
};
pms.push(pmOption);
}
}
}
// bring all the arrays together in the right order
const validTimes = [...midnight, ...ams, ...noon, ...pms];
// determine if an unsupported value already exists on the send & add that to the top of the option list
// example: if the Send was created with a different client
if (field === DateField.ExpirationDate && this.initialExpirationDate != null && this.editMode) {
const previousValue: TimeOption = {
twelveHour: this.datePipe.transform(this.initialExpirationDate, "hh:mm a"),
twentyFourHour: this.datePipe.transform(this.initialExpirationDate, "HH:mm"),
};
return [previousValue, { twelveHour: null, twentyFourHour: null }, ...validTimes];
} else if (
field === DateField.DeletionDate &&
this.initialDeletionDate != null &&
this.editMode
) {
const previousValue: TimeOption = {
twelveHour: this.datePipe.transform(this.initialDeletionDate, "hh:mm a"),
twentyFourHour: this.datePipe.transform(this.initialDeletionDate, "HH:mm"),
};
return [previousValue, ...validTimes];
} else {
return [{ twelveHour: null, twentyFourHour: null }, ...validTimes];
}
}
}

View File

@ -6,7 +6,9 @@ export type InputTypes =
| "email"
| "checkbox"
| "search"
| "file";
| "file"
| "date"
| "time";
export abstract class BitFormFieldControl {
ariaDescribedBy: string;