1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

api actions on add/edit

This commit is contained in:
Kyle Spearrin
2018-01-31 12:52:12 -05:00
parent 619d3fd075
commit 1c41dfa196
12 changed files with 324 additions and 290 deletions

View File

@@ -1,5 +1,5 @@
<div id="login"> <div id="login">
<form #form (ngSubmit)="onSubmit()" [appApiForm]="formPromise"> <form #form (ngSubmit)="onSubmit()" [appApiAction]="formPromise">
<div class="content"> <div class="content">
<img src="../../images/logo@2x.png" alt="bitwarden"> <img src="../../images/logo@2x.png" alt="bitwarden">
<p>{{'loginOrCreateNewAccount' | i18n}}</p> <p>{{'loginOrCreateNewAccount' | i18n}}</p>

View File

@@ -11,7 +11,7 @@ import { ServicesModule } from './services/services.module';
import { ToasterModule } from 'angular2-toaster'; import { ToasterModule } from 'angular2-toaster';
import { AddEditComponent } from './vault/add-edit.component'; import { AddEditComponent } from './vault/add-edit.component';
import { ApiFormDirective } from './directives/api-form.directive'; import { ApiActionDirective } from './directives/api-action.directive';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AttachmentsComponent } from './vault/attachments.component'; import { AttachmentsComponent } from './vault/attachments.component';
import { AutofocusDirective } from './directives/autofocus.directive'; import { AutofocusDirective } from './directives/autofocus.directive';
@@ -47,7 +47,7 @@ import { ViewComponent } from './vault/view.component';
], ],
declarations: [ declarations: [
AddEditComponent, AddEditComponent,
ApiFormDirective, ApiActionDirective,
AppComponent, AppComponent,
AttachmentsComponent, AttachmentsComponent,
AutofocusDirective, AutofocusDirective,

View File

@@ -8,21 +8,21 @@ import {
import { ValidationService } from '../services/validation.service'; import { ValidationService } from '../services/validation.service';
@Directive({ @Directive({
selector: '[appApiForm]', selector: '[appApiAction]',
}) })
export class ApiFormDirective implements OnChanges { export class ApiActionDirective implements OnChanges {
@Input() appApiForm: Promise<any>; @Input() appApiAction: Promise<any>;
constructor(private el: ElementRef, private validationService: ValidationService) { } constructor(private el: ElementRef, private validationService: ValidationService) { }
ngOnChanges(changes: any) { ngOnChanges(changes: any) {
if (this.appApiForm == null || this.appApiForm.then == null) { if (this.appApiAction == null || this.appApiAction.then == null) {
return; return;
} }
this.el.nativeElement.loading = true; this.el.nativeElement.loading = true;
this.appApiForm.then((response: any) => { this.appApiAction.then((response: any) => {
this.el.nativeElement.loading = false; this.el.nativeElement.loading = false;
}, (e: any) => { }, (e: any) => {
this.el.nativeElement.loading = false; this.el.nativeElement.loading = false;

View File

@@ -1,8 +1,9 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content"> <div class="content">
<div class="inner-content" *ngIf="cipher"> <div class="inner-content" *ngIf="cipher">
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
{{'itemInformation' | i18n}} {{title}}
</div> </div>
<div class="box-content"> <div class="box-content">
<div class="box-content-row" *ngIf="!editMode" appBoxRow> <div class="box-content-row" *ngIf="!editMode" appBoxRow>
@@ -29,17 +30,18 @@
<div class="box-content-row box-content-row-flex" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main"> <div class="row-main">
<label for="loginPassword">{{'password' | i18n}}</label> <label for="loginPassword">{{'password' | i18n}}</label>
<input id="loginPassword" class="monospaced" type="{{showPassword ? 'text' : 'password'}}" <input id="loginPassword" class="monospaced"
name="Login.Password" [(ngModel)]="cipher.login.password"> type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
[(ngModel)]="cipher.login.password">
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick title="{{'toggleVisibility' | i18n}}" <a class="row-btn" href="#" appStopClick appBlurClick
(click)="togglePassword()"> title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
<i class="fa fa-lg" <i class="fa fa-lg"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i> [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a> </a>
<a class="row-btn" href="#" appStopClick appBlurClick title="{{'generatePassword' | i18n}}" <a class="row-btn" href="#" appStopClick appBlurClick
(click)="generatePassword()"> title="{{'generatePassword' | i18n}}" (click)="generatePassword()">
<i class="fa fa-lg fa-refresh"></i> <i class="fa fa-lg fa-refresh"></i>
</a> </a>
</div> </div>
@@ -229,8 +231,8 @@
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox" <input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean"> [(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean">
<div class="action-buttons" *ngIf="f.type === fieldType.Hidden"> <div class="action-buttons" *ngIf="f.type === fieldType.Hidden">
<a class="row-btn" href="#" appStopClick appBlurClick title="{{'toggleVisibility' | i18n}}" <a class="row-btn" href="#" appStopClick appBlurClick
(click)="toggleFieldValue(f)"> title="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)">
<i class="fa fa-lg" <i class="fa fa-lg"
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}"></i> [ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}"></i>
</a> </a>
@@ -242,7 +244,7 @@
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}} <i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}}
</a> </a>
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label> <label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
<select id="addFieldType" [(ngModel)]="addFieldType" class="field-type"> <select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option> <option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option>
</select> </select>
</div> </div>
@@ -251,15 +253,20 @@
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button appBlurClick (click)="save()" class="primary" title="{{'save' | i18n}}"> <button appBlurClick type="submit" class="primary" title="{{'save' | i18n}}" [disabled]="form.loading">
<i class="fa fa-save fa-lg"></i> {{'save' | i18n}} <i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
</button> </button>
<button appBlurClick (click)="cancel()" title="{{'cancel' | i18n}}"> <button appBlurClick type="button" (click)="cancel()" title="{{'cancel' | i18n}}">
{{'cancel' | i18n}} {{'cancel' | i18n}}
</button> </button>
<div class="right"> <div class="right">
<button appBlurClick (click)="delete()" class="danger" title="{{'delete' | i18n}}" *ngIf="editMode"> <button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger"
<i class="fa fa-trash-o fa-lg"></i> title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button> </button>
</div> </div>
</div> </div>
</form>

View File

@@ -44,6 +44,9 @@ export class AddEditComponent implements OnChanges {
editMode: boolean = false; editMode: boolean = false;
cipher: CipherView; cipher: CipherView;
folders: FolderView[]; folders: FolderView[];
title: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
showPassword: boolean = false; showPassword: boolean = false;
cipherType = CipherType; cipherType = CipherType;
fieldType = FieldType; fieldType = FieldType;
@@ -109,9 +112,11 @@ export class AddEditComponent implements OnChanges {
if (this.editMode) { if (this.editMode) {
this.editMode = true; this.editMode = true;
this.title = this.i18nService.t('editItem');
const cipher = await this.cipherService.get(this.cipherId); const cipher = await this.cipherService.get(this.cipherId);
this.cipher = await cipher.decrypt(); this.cipher = await cipher.decrypt();
} else { } else {
this.title = this.i18nService.t('addItem');
this.cipher = new CipherView(); this.cipher = new CipherView();
this.cipher.folderId = this.folderId; this.cipher.folderId = this.folderId;
this.cipher.type = CipherType.Login; this.cipher.type = CipherType.Login;
@@ -125,7 +130,7 @@ export class AddEditComponent implements OnChanges {
this.folders = await this.folderService.getAllDecrypted(); this.folders = await this.folderService.getAllDecrypted();
} }
async save() { async submit() {
if (this.cipher.name == null || this.cipher.name === '') { if (this.cipher.name == null || this.cipher.name === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired')); this.i18nService.t('nameRequired'));
@@ -133,10 +138,14 @@ export class AddEditComponent implements OnChanges {
} }
const cipher = await this.cipherService.encrypt(this.cipher); const cipher = await this.cipherService.encrypt(this.cipher);
await this.cipherService.saveWithServer(cipher);
try {
this.formPromise = this.cipherService.saveWithServer(cipher);
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' });
this.toasterService.popAsync('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); this.toasterService.popAsync('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem'));
this.onSavedCipher.emit(this.cipher); this.onSavedCipher.emit(this.cipher);
} catch { }
} }
addField() { addField() {
@@ -169,10 +178,13 @@ export class AddEditComponent implements OnChanges {
return; return;
} }
await this.cipherService.deleteWithServer(this.cipher.id); try {
this.deletePromise = this.cipherService.deleteWithServer(this.cipher.id);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); this.analytics.eventTrack.next({ action: 'Deleted Cipher' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem'));
this.onDeletedCipher.emit(this.cipher); this.onDeletedCipher.emit(this.cipher);
} catch { }
} }
generatePassword() { generatePassword() {

View File

@@ -1,6 +1,6 @@
<div class="modal fade"> <div class="modal fade">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content"> <form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-body"> <div class="modal-body">
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
@@ -16,17 +16,20 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="primary" appBlurClick (click)="save()"> <button appBlurClick type="submit" class="primary" title="{{'save' | i18n}}" [disabled]="form.loading">
<i class="fa fa-lg fa-save"></i> {{'save' | i18n}} <i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
</button> </button>
<button type="button" data-dismiss="modal">{{'close' | i18n}}</button> <button type="button" data-dismiss="modal">{{'close' | i18n}}</button>
<div class="right"> <div class="right">
<button appBlurClick (click)="delete()" class="danger" title="{{'delete' | i18n}}" <button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger"
*ngIf="editMode"> title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
<i class="fa fa-trash-o fa-lg"></i> [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </form>
</div> </div>
</div> </div>

View File

@@ -28,6 +28,8 @@ export class FolderAddEditComponent implements OnInit {
editMode: boolean = false; editMode: boolean = false;
folder: FolderView = new FolderView(); folder: FolderView = new FolderView();
title: string; title: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
constructor(private folderService: FolderService, private i18nService: I18nService, constructor(private folderService: FolderService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { } private analytics: Angulartics2, private toasterService: ToasterService) { }
@@ -45,19 +47,22 @@ export class FolderAddEditComponent implements OnInit {
} }
} }
async save() { async submit() {
if (this.folder.name == null || this.folder.name === '') { if (this.folder.name == null || this.folder.name === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired')); this.i18nService.t('nameRequired'));
return; return;
} }
try {
const folder = await this.folderService.encrypt(this.folder); const folder = await this.folderService.encrypt(this.folder);
await this.folderService.saveWithServer(folder); this.formPromise = this.folderService.saveWithServer(folder);;
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Folder' : 'Added Folder' }); this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Folder' : 'Added Folder' });
this.toasterService.popAsync('success', null, this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder')); this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder'));
this.onSavedFolder.emit(this.folder); this.onSavedFolder.emit(this.folder);
} catch { }
} }
async delete() { async delete() {
@@ -65,9 +70,12 @@ export class FolderAddEditComponent implements OnInit {
return; return;
} }
await this.folderService.deleteWithServer(this.folder.id); try {
this.deletePromise = this.folderService.deleteWithServer(this.folder.id);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Folder' }); this.analytics.eventTrack.next({ action: 'Deleted Folder' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedFolder')); this.toasterService.popAsync('success', null, this.i18nService.t('deletedFolder'));
this.onDeletedFolder.emit(this.folder); this.onDeletedFolder.emit(this.folder);
} catch { }
} }
} }

View File

@@ -45,7 +45,7 @@
<li *ngFor="let f of folders" [ngClass]="{active: selectedFolder && f.id === selectedFolderId}"> <li *ngFor="let f of folders" [ngClass]="{active: selectedFolder && f.id === selectedFolderId}">
<a href="#" appStopClick appBlurClick (click)="folder(f)"> <a href="#" appStopClick appBlurClick (click)="folder(f)">
<i class="fa-li fa fa-caret-right"></i> {{f.name}} <i class="fa-li fa fa-caret-right"></i> {{f.name}}
<span appStopProp (click)="editFolder(f)" title="{{'editFolder' | i18n}}"> <span appStopProp appStopClick (click)="editFolder(f)" title="{{'editFolder' | i18n}}">
<i class="fa fa-pencil fa-fw"></i> <i class="fa fa-pencil fa-fw"></i>
</span> </span>
</a> </a>

View File

@@ -231,5 +231,7 @@
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button appBlurClick (click)="edit()" title="{{'editItem' | i18n}}">{{'edit' | i18n}}</button> <button appBlurClick class="primary" (click)="edit()" title="{{'edit' | i18n}}">
<i class="fa fa-pencil fa-fw fa-lg"></i>
</button>
</div> </div>

View File

@@ -59,9 +59,6 @@
"editItem": { "editItem": {
"message": "Edit Item" "message": "Edit Item"
}, },
"itemInformation": {
"message": "Item Information"
},
"emailAddress": { "emailAddress": {
"message": "Email Address" "message": "Email Address"
}, },

View File

@@ -21,7 +21,7 @@
} }
&[disabled] { &[disabled] {
background-color: darken($button-backgound-color, 1.5%); opacity: 0.65;
cursor: default; cursor: default;
} }

View File

@@ -125,6 +125,11 @@
margin-bottom: 30px; margin-bottom: 30px;
} }
} }
> form {
display: flex;
flex-direction: column;
}
} }
#groupings, #items, #details { #groupings, #items, #details {