bells and whistles for Send

This commit is contained in:
addison 2021-02-18 13:03:20 -05:00
parent 4a8cf62495
commit ac807b6d54
7 changed files with 170 additions and 78 deletions

View File

@ -197,6 +197,14 @@ export class AppComponent implements OnInit {
break; break;
case 'ssoCallback': case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } }); this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
case 'premiumRequired':
const premiumConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (premiumConfirmed) {
this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
}
break;
default: default:
} }
}); });

View File

@ -1,6 +1,12 @@
<!-- Fix the menu bar -->
<form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content"> <div class="content">
<div class="inner-content" *ngIf="send"> <div class="inner-content" *ngIf="send">
<div class="box">
<app-callout *ngIf="disableSend">
<span>{{'sendDisabledWarning' | i18n}}</span>
</app-callout>
</div>
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
{{title}} {{title}}
@ -8,7 +14,7 @@
<div class="box-content"> <div class="box-content">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="name">{{'name' | i18n}}</label> <label for="name">{{'name' | i18n}}</label>
<input id="name" type="text" name="Name" [(ngModel)]="send.name" appAutofocus> <input id="name" type="text" name="Name" [(ngModel)]="send.name" appAutofocus [readOnly]="disableSend">
</div> </div>
</div> </div>
</div> </div>
@ -35,18 +41,18 @@
<ng-container *ngIf="editMode && send.type === sendType.File"> <ng-container *ngIf="editMode && send.type === sendType.File">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="file">{{'file' | i18n}}</label> <label for="file">{{'file' | i18n}}</label>
<input id="file" type="text" name="file" [(ngModel)]="send.file.fileName" readonly> <input id="file" type="text" name="file" [(ngModel)]="send.file.fileName" readonly [disabled]="disableSend">
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="send.type === sendType.Text"> <ng-container *ngIf="send.type === sendType.Text">
<div class="box-content-row" appBoxRow *ngIf="send.type === sendType.Text"> <div class="box-content-row" appBoxRow *ngIf="send.type === sendType.Text">
<label for="text">{{'text' | i18n}}</label> <label for="text">{{'text' | i18n}}</label>
<textarea id="text" name="text" [(ngModel)]="send.text.text" rows="6"></textarea> <textarea id="text" name="text" [(ngModel)]="send.text.text" rows="6" [readOnly]="disableSend"></textarea>
<div class="subtext">{{'sendTextDesc' | i18n}}</div> <div class="subtext">{{'sendTextDesc' | i18n}}</div>
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="send.type === sendType.Text"> <div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="send.type === sendType.Text">
<label for="hideText">{{'textHiddenByDefault' | i18n}}</label> <label for="hideText">{{'textHiddenByDefault' | i18n}}</label>
<input id="hideText" name="hideText" type="checkbox" [(ngModel)]="send.text.hidden"> <input id="hideText" name="hideText" type="checkbox" [(ngModel)]="send.text.hidden" [disabled]="disableSend">
</div> </div>
</ng-container> </ng-container>
</div> </div>
@ -54,99 +60,123 @@
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
{{'options' | i18n}} {{'options' | i18n}}
<a class="toggle" href="#" appStopClick appBlurClick role="button" (click)="toggleOptions()">
<i class="fa fa-lg" aria-hidden="true" [ngClass]="{'fa-chevron-down': !showOptions, 'fa-chevron-up': showOptions}"></i>
</a>
</div> </div>
<div class="box-content"> </div>
<div class="box-content-row" appBoxRow> <div [hidden]="!showOptions">
<label for="deletionDate">{{'deletionDate' | i18n}}</label> <div class="box">
<input id="deletionDate" type="datetime-local" name="deletionDate" <div class="box-content">
[(ngModel)]="deletionDate" required placeholder="MM/DD/YYYY HH:MM AM/PM" *ngIf="editMode"> <div class="box-content-row" appBoxRow *ngIf="!editMode">
<select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect" required *ngIf="!editMode"> <label for="deletionDate">{{'deletionDate' | i18n}}</label>
<option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}} <select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect" required [readOnly]="disableSend">
</option> <option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}}
</select> </option>
<div class="subtext">{{'deletionDateDesc' | i18n}}</div> </select>
<div class="subtext">{{'deletionDateDesc' | i18n}}</div>
</div>
<div class="box-content-row" *ngIf="deletionDateSelect === 0 || editMode">
<label *ngIf="editMode" for="deletionDateCustom">{{'deletionDate' | i18n}}</label>
<input id="deletionDateCustom" type="datetime-local" name="deletionDate"
[(ngModel)]="deletionDate" required placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
<div class="subtext">{{'deletionDateDesc' | i18n}}</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<select id="expirationDate" name="expirationDateSelect" [(ngModel)]="expirationDateSelect" required>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<div class="subtext">{{'expirationDateDesc' | i18n}}</div>
</div>
<div class="box-content-row" *ngIf="expirationDateSelect === 0 || editMode">
<label *ngIf="editMode" for="expirationDateCustom">{{'expirationDate' | i18n}}</label>
<input id="expirationDateCustom" type="datetime-local" name="expirationDate"
[(ngModel)]="expirationDate" required placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
<div *ngIf="editMode" class="subtext">{{'expirationDateDesc' | i18n}}</div>
</div>
</div> </div>
<div class="box-content-row" *ngIf="deletionDateSelect === 0 && !editMode"> </div>
<input id="deletionDateCustom" type="datetime-local" name="deletionDate" <div class="box">
[(ngModel)]="deletionDate" required placeholder="MM/DD/YYYY HH:MM AM/PM"> <div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" type="number" name="maxAccessCount" [(ngModel)]="send.maxAccessCount" [readOnly]="disableSend">
<div class="subtext">{{'maxAccessCountDesc' | i18n}}</div>
</div>
<div *ngIf="editMode" class="box-content-row" appBoxRow>
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" type="text" name="accessCount" [(ngModel)]="send.accessCount" readonly>
</div>
</div> </div>
<div class="box-content-row" appBoxRow> </div>
<label for="expirationDate">{{'expirationDate' | i18n}}</label> <div class="box">
<input id="expirationDate" type="datetime-local" name="expirationDate" [(ngModel)]="expirationDate" *ngIf="editMode"> <div class="box-content">
<select id="expirationDate" name="expirationDateSelect" [(ngModel)]="expirationDateSelect" required *ngIf="!editMode"> <div class="box-content-row box-content-row-flex" appBoxRow>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}} <div class="row-main">
</option> <label for="password">{{(hasPassword ? 'newPassword' : 'password') | i18n}}</label>
</select> <input id="password" name="password" type="{{showPassword ? 'text' : 'password'}}" [(ngModel)]="password" [readOnly]="disableSend">
<div class="subtext">{{'expirationDateDesc' | i18n}}</div> <div class="subtext">{{'sendPasswordDesc' | i18n}}</div>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick role="button"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePasswordVisible()" [disabled]="disableSend">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a>
</div>
</div>
</div> </div>
<div class="box-content-row" *ngIf="expirationDateSelect === 0 && !editMode"> </div>
<input id="expirationDateCustom" type="datetime-local" name="expirationDate" <div class="box">
[(ngModel)]="expirationDate" required placeholder="MM/DD/YYYY HH:MM AM/PM"> <div class="box-header">
{{'notes' | i18n}}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea id="notes" name="notes" [(ngModel)]="send.notes" rows="6" [readOnly]="disableSend"></textarea>
<small class="subtext">{{'sendNotesDesc' | i18n}}</small>
</div>
</div>
</div>
<div class="box">
<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">
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="box"> <div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" type="number" name="maxAccessCount" [(ngModel)]="send.maxAccessCount">
<div class="subtext">{{'maxAccessCountDesc' | i18n}}</div>
</div>
<div *ngIf="editMode" class="box-content-row" appBoxRow>
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" type="text" name="accessCount" [(ngModel)]="send.accessCount" readonly>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="password">{{(hasPassword ? 'newPassword' : 'password') | i18n}}</label>
<input id="password" type="password" name="password" [(ngModel)]="password">
<div class="subtext">{{'sendPasswordDesc' | i18n}}</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
{{'notes' | i18n}}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea id="notes" name="notes" [(ngModel)]="send.notes" rows="6"></textarea>
<small class="subtext">{{'sendNotesDesc' | i18n}}</small>
</div>
</div>
</div>
<div class="box">
<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">
</div>
</div>
</div>
<div class="box" *ngIf="editMode">
<div class="box-header"> <div class="box-header">
{{'share' | i18n}} {{'share' | i18n}}
</div> </div>
<div class="box-content"> <div class="box-content">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="url">{{'sendLink' | i18n}}</label> <label for="link">{{'sendLinkLabel' | i18n}}</label>
<input id="url" name="url" [ngModel]="link" readonly> <input id="link" name="link" [ngModel]="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">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button appBlurClick type="submit" class="primary" appA11yTitle="{{'save' | i18n}}"> <button appBlurClick type="submit" class="primary" appA11yTitle="{{'save' | i18n}}" *ngIf="!disableSend">
<i class="fa fa-save fa-lg fa-fw" aria-hidden="true"></i> <i class="fa fa-save fa-lg fa-fw" aria-hidden="true"></i>
</button> </button>
<button appBlurClick type="button" (click)="cancel()"> <button appBlurClick type="button" (click)="cancel()">
{{'cancel' | i18n}} {{'cancel' | i18n}}
</button> </button>
<div class="right"> <div class="right">
<button appBlurClick type="button" (click)="copyLinkToClipboard(link)" appA11yTitle="{{'copySendLinkToClipboard' | i18n}}" *ngIf="editMode">
<i class="fa fa-copy fa-lg fa-fw" aria-hidden="true"></i>
</button>
<button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger" <button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading" appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise"> [appApiAction]="deletePromise">

View File

@ -17,6 +17,7 @@ import { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/componen
templateUrl: 'add-edit.component.html', templateUrl: 'add-edit.component.html',
}) })
export class AddEditComponent extends BaseAddEditComponent { export class AddEditComponent extends BaseAddEditComponent {
showOptions = false;
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, datePipe: DatePipe, environmentService: EnvironmentService, datePipe: DatePipe,
sendService: SendService, userService: UserService, sendService: SendService, userService: UserService,
@ -37,4 +38,14 @@ export class AddEditComponent extends BaseAddEditComponent {
cancel() { cancel() {
this.onCancelled.emit(this.send); this.onCancelled.emit(this.send);
} }
toggleOptions() {
this.showOptions = !this.showOptions;
}
copyLinkToClipboard(link: string) {
super.copyLinkToClipboard(link);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
}
} }

View File

@ -42,7 +42,7 @@
<div class="list" *ngIf="filteredSends.length" infiniteScroll [infiniteScrollDistance]="1" <div class="list" *ngIf="filteredSends.length" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollContainer]="'#items .content'" [fromRoot]="true" (scrolled)="loadMore()"> [infiniteScrollContainer]="'#items .content'" [fromRoot]="true" (scrolled)="loadMore()">
<a *ngFor="let s of filteredSends" appStopClick (click)="selectSend(s.id)" <a *ngFor="let s of filteredSends" appStopClick (click)="selectSend(s.id)"
href="#" title="{{'viewItem' | i18n}}" href="#" title="{{'viewItem' | i18n}}" (contextmenu)="viewSendMenu(s)"
[ngClass]="{'active': s.id === sendId}"> [ngClass]="{'active': s.id === sendId}">
<div class="icon" aria-hidden="true"> <div class="icon" aria-hidden="true">
<i class="fa fa-fw fa-lg" [ngClass]="s.type == 0 ? 'fa-file-o' : 'fa-file-text-o'"></i> <i class="fa fa-fw fa-lg" [ngClass]="s.type == 0 ? 'fa-file-o' : 'fa-file-text-o'"></i>

View File

@ -1,3 +1,5 @@
import { remote } from 'electron';
import { import {
Component, Component,
NgZone, NgZone,
@ -77,10 +79,6 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
} }
} }
editSend(send: SendView) {
return;
}
cancel(s: SendView) { cancel(s: SendView) {
this.action = Action.None; this.action = Action.None;
this.sendId = null; this.sendId = null;
@ -112,4 +110,20 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
get selectedSendType() { get selectedSendType() {
return this.sends.find(s => s.id === this.sendId)?.type; return this.sends.find(s => s.id === this.sendId)?.type;
} }
viewSendMenu(send: SendView) {
const menu = new remote.Menu();
menu.append(new remote.MenuItem({
label: this.i18nService.t('copyLink'),
click: () => this.copy(send)
}));
menu.append(new remote.MenuItem({
label: this.i18nService.t('delete'),
click: async () => {
await this.delete(send);
await this.deletedSend(send);
}
}));
menu.popup({ window: remote.getCurrentWindow() });
}
} }

View File

@ -1562,6 +1562,10 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendLink": { "sendLink": {
"message": "Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLinkLabel": {
"message": "Send Link", "message": "Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
@ -1616,5 +1620,20 @@
"deleteSendConfirmation": { "deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?", "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." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLinkToClipboard": {
"message": "Copy Send link to clipboard",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLinkOnSave": {
"message": "Copy the link to share this Send to my clipboard upon save."
},
"sendDisabled": {
"message": "Send disabled",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDisabledWarning": {
"message": "Due to an enterprise policy, you are only able to delete an existing Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
} }
} }

View File

@ -7,6 +7,7 @@
.box-header { .box-header {
margin: 0 10px 5px 10px; margin: 0 10px 5px 10px;
text-transform: uppercase; text-transform: uppercase;
display: flex;
@include themify($themes) { @include themify($themes) {
color: themed('headingColor'); color: themed('headingColor');
@ -21,6 +22,15 @@
color: themed('headingColor'); color: themed('headingColor');
} }
} }
.toggle {
background-color: transparent;
margin-left: 4px;
font-size: $font-size-small;
@include themify($themes) {
color: themed('headingColor');
}
}
} }
.box-content { .box-content {