mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-25 16:59:17 +01:00
Merge pull request #511 from bitwarden/soft-delete-chad
[Soft Delete] - Added trash and related functionality to web vault
This commit is contained in:
commit
9f1b8ae58f
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit 72e3893f8eee79f1e3678839aa194f1096c343ea
|
||||
Subproject commit 3a10c1ff3027832953094b2f7edddb2361119b09
|
@ -145,6 +145,7 @@ import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
||||
import { BulkMoveComponent } from './vault/bulk-move.component';
|
||||
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
||||
import { BulkShareComponent } from './vault/bulk-share.component';
|
||||
import { CiphersComponent } from './vault/ciphers.component';
|
||||
import { CollectionsComponent } from './vault/collections.component';
|
||||
@ -257,6 +258,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
BreachReportComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CalloutComponent,
|
||||
ChangeEmailComponent,
|
||||
@ -375,6 +377,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
AttachmentsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CollectionsComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
|
@ -94,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.deleteCipher();
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(this.cipherId);
|
||||
return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId)
|
||||
: this.apiService.putDeleteCipherAdmin(this.cipherId);
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
await super.load(filter);
|
||||
await super.load(filter, this.deleted);
|
||||
return;
|
||||
}
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
@ -65,13 +65,19 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
this.searchPending = false;
|
||||
let filteredCiphers = this.allCiphers;
|
||||
if (this.filter != null) {
|
||||
filteredCiphers = filteredCiphers.filter(this.filter);
|
||||
}
|
||||
|
||||
if (this.searchText == null || this.searchText.trim().length < 2) {
|
||||
this.ciphers = filteredCiphers;
|
||||
this.ciphers = filteredCiphers.filter((c) => {
|
||||
if (c.isDeleted !== this.deleted) {
|
||||
return false;
|
||||
}
|
||||
return this.filter == null || this.filter(c);
|
||||
});
|
||||
} else {
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
|
||||
if (this.filter != null) {
|
||||
filteredCiphers = filteredCiphers.filter(this.filter);
|
||||
}
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
|
||||
}
|
||||
await this.resetPaging();
|
||||
}
|
||||
@ -86,9 +92,9 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
|
||||
protected deleteCipher(id: string) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.deleteCipher(id);
|
||||
return super.deleteCipher(id, this.deleted);
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(id);
|
||||
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
|
@ -1,9 +1,10 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false"
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false" [showTrash]="true"
|
||||
(onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)"
|
||||
(onTrashClicked)="filterDeleted()">
|
||||
</app-org-vault-groupings>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
@ -18,7 +19,8 @@
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
|
||||
*ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
@ -33,4 +35,4 @@
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
|
@ -49,8 +49,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
|
||||
organization: Organization;
|
||||
collectionId: string;
|
||||
type: CipherType;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
deleted: boolean = false;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
@ -61,7 +62,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const queryParams = this.route.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.groupingsComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
@ -92,7 +93,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.reload();
|
||||
} else {
|
||||
if (qParams.type) {
|
||||
if (qParams.deleted) {
|
||||
this.groupingsComponent.selectedTrash = true;
|
||||
await this.filterDeleted(true);
|
||||
} else if (qParams.type) {
|
||||
const t = parseInt(qParams.type, null);
|
||||
this.groupingsComponent.selectedType = t;
|
||||
await this.filterCipherType(t, true);
|
||||
@ -116,6 +120,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
if (queryParams != null) {
|
||||
queryParams.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -125,6 +133,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async clearGroupingFilters() {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
|
||||
await this.ciphersComponent.applyFilter();
|
||||
this.clearFilters();
|
||||
@ -133,6 +142,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async filterCipherType(type: CipherType, load = false) {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
|
||||
const filter = (c: CipherView) => c.type === type;
|
||||
if (load) {
|
||||
@ -147,6 +157,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async filterCollection(collectionId: string, load = false) {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
const filter = (c: CipherView) => {
|
||||
if (collectionId === 'unassigned') {
|
||||
@ -165,6 +176,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.go();
|
||||
}
|
||||
|
||||
async filterDeleted(load: boolean = false) {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.deleted = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchTrash');
|
||||
if (load) {
|
||||
await this.ciphersComponent.reload(null, true);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(null);
|
||||
}
|
||||
this.clearFilters();
|
||||
this.deleted = true;
|
||||
this.go();
|
||||
}
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
@ -255,6 +280,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
@ -299,6 +328,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private clearFilters() {
|
||||
this.collectionId = null;
|
||||
this.type = null;
|
||||
this.deleted = false;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
@ -306,6 +336,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParams = {
|
||||
type: this.type,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -73,8 +73,14 @@ export class EventService {
|
||||
msg = this.i18nService.t('editedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_Deleted:
|
||||
msg = this.i18nService.t('permanentlyDeletedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_SoftDeleted:
|
||||
msg = this.i18nService.t('deletedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_Restored:
|
||||
msg = this.i18nService.t('restoredItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_AttachmentCreated:
|
||||
msg = this.i18nService.t('createdAttachmentForItem', this.formatCipherId(ev, options));
|
||||
break;
|
||||
|
@ -62,6 +62,10 @@ export class CipherReportComponent {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
|
@ -12,7 +12,8 @@
|
||||
<div class="row" *ngIf="!editMode">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{'whatTypeOfItem' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control">
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -21,11 +22,12 @@
|
||||
<div class="col-6 form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="cipher.name"
|
||||
required>
|
||||
required [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group" *ngIf="!organization">
|
||||
<label for="folder">{{'folder' | i18n}}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control">
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -37,8 +39,8 @@
|
||||
<label for="loginUsername">{{'username' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<input id="loginUsername" class="form-control" type="text" name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username" appInputVerbatim>
|
||||
<div class="input-group-append">
|
||||
[(ngModel)]="cipher.login.username" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append" *ngIf="!cipher.isDeleted">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyUsername' | i18n}}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')" tabindex="-1">
|
||||
@ -50,7 +52,7 @@
|
||||
<div class="col-6 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="loginPassword">{{'password' | i18n}}</label>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ml-auto d-flex" *ngIf="!cipher.isDeleted">
|
||||
<a href="#" class="d-block mr-2" appStopClick
|
||||
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()">
|
||||
<i class="fa fa-lg fa-fw fa-refresh" aria-hidden="true"></i>
|
||||
@ -68,7 +70,8 @@
|
||||
<div class="input-group">
|
||||
<input id="loginPassword" class="form-control text-monospace"
|
||||
type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password">
|
||||
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()"
|
||||
@ -89,7 +92,7 @@
|
||||
<div class="col-6 form-group">
|
||||
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
|
||||
<input id="loginTotp" type="text" name="Login.Totp" class="form-control text-monospace"
|
||||
[(ngModel)]="cipher.login.totp" appInputVerbatim>
|
||||
[(ngModel)]="cipher.login.totp" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}">
|
||||
<div *ngIf="!cipher.login.totp || !totpCode">
|
||||
@ -132,7 +135,7 @@
|
||||
<label for="loginUri{{i}}">{{'uriPosition' | i18n : (i + 1)}}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="loginUri{{i}}" type="text"
|
||||
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri"
|
||||
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri" [disabled]="cipher.isDeleted"
|
||||
placeholder="{{'ex' | i18n}} https://google.com" appInputVerbatim>
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@ -160,19 +163,20 @@
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<select class="form-control" id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match"
|
||||
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)">
|
||||
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeUri(u)"
|
||||
appA11yTitle="{{'remove' | i18n}}">
|
||||
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3">
|
||||
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newUri' | i18n}}
|
||||
</a>
|
||||
</ng-container>
|
||||
@ -182,12 +186,13 @@
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardCardholderName">{{'cardholderName' | i18n}}</label>
|
||||
<input id="cardCardholderName" class="form-control" type="text"
|
||||
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName">
|
||||
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName"
|
||||
[disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardBrand">{{'brand' | i18n}}</label>
|
||||
<select id="cardBrand" class="form-control" name="Card.Brand"
|
||||
[(ngModel)]="cipher.card.brand">
|
||||
[(ngModel)]="cipher.card.brand" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -197,7 +202,7 @@
|
||||
<label for="cardNumber">{{'number' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<input id="cardNumber" class="form-control" type="text" name="Card.Number"
|
||||
[(ngModel)]="cipher.card.number" appInputVerbatim>
|
||||
[(ngModel)]="cipher.card.number" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyNumber' | i18n}}"
|
||||
@ -210,14 +215,15 @@
|
||||
<div class="col form-group">
|
||||
<label for="cardExpMonth">{{'expirationMonth' | i18n}}</label>
|
||||
<select id="cardExpMonth" class="form-control" name="Card.ExpMonth"
|
||||
[(ngModel)]="cipher.card.expMonth">
|
||||
[(ngModel)]="cipher.card.expMonth" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col form-group">
|
||||
<label for="cardExpYear">{{'expirationYear' | i18n}}</label>
|
||||
<input id="cardExpYear" class="form-control" type="text" name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019">
|
||||
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019"
|
||||
[disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -226,7 +232,8 @@
|
||||
<div class="input-group">
|
||||
<input id="cardCode" class="form-control text-monospace"
|
||||
type="{{showCardCode ? 'text' : 'password'}}" name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password">
|
||||
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleCardCode()"
|
||||
@ -250,7 +257,7 @@
|
||||
<div class="col-4 form-group">
|
||||
<label for="idTitle">{{'title' | i18n}}</label>
|
||||
<select id="idTitle" class="form-control" name="Identity.Title"
|
||||
[(ngModel)]="cipher.identity.title">
|
||||
[(ngModel)]="cipher.identity.title" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -259,107 +266,107 @@
|
||||
<div class="col-4 form-group">
|
||||
<label for="idFirstName">{{'firstName' | i18n}}</label>
|
||||
<input id="idFirstName" class="form-control" type="text" name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName">
|
||||
[(ngModel)]="cipher.identity.firstName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idMiddleName">{{'middleName' | i18n}}</label>
|
||||
<input id="idMiddleName" class="form-control" type="text" name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName">
|
||||
[(ngModel)]="cipher.identity.middleName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLastName">{{'lastName' | i18n}}</label>
|
||||
<input id="idLastName" class="form-control" type="text" name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName">
|
||||
[(ngModel)]="cipher.identity.lastName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idUsername">{{'username' | i18n}}</label>
|
||||
<input id="idUsername" class="form-control" type="text" name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.username" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idCompany">{{'company' | i18n}}</label>
|
||||
<input id="idCompany" class="form-control" type="text" name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company">
|
||||
[(ngModel)]="cipher.identity.company" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idSsn">{{'ssn' | i18n}}</label>
|
||||
<input id="idSsn" class="form-control" type="text" name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.ssn" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idPassportNumber">{{'passportNumber' | i18n}}</label>
|
||||
<input id="idPassportNumber" class="form-control" type="text" name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLicenseNumber">{{'licenseNumber' | i18n}}</label>
|
||||
<input id="idLicenseNumber" class="form-control" type="text" name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idEmail">{{'email' | i18n}}</label>
|
||||
<input id="idEmail" class="form-control" type="text" name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.email" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPhone">{{'phone' | i18n}}</label>
|
||||
<input id="idPhone" class="form-control" type="text" name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone">
|
||||
[(ngModel)]="cipher.identity.phone" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress1">{{'address1' | i18n}}</label>
|
||||
<input id="idAddress1" class="form-control" type="text" name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1">
|
||||
[(ngModel)]="cipher.identity.address1" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress2">{{'address2' | i18n}}</label>
|
||||
<input id="idAddress2" class="form-control" type="text" name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2">
|
||||
[(ngModel)]="cipher.identity.address2" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress3">{{'address3' | i18n}}</label>
|
||||
<input id="idAddress3" class="form-control" type="text" name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3">
|
||||
[(ngModel)]="cipher.identity.address3" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCity">{{'cityTown' | i18n}}</label>
|
||||
<input id="idCity" class="form-control" type="text" name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city">
|
||||
[(ngModel)]="cipher.identity.city" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idState">{{'stateProvince' | i18n}}</label>
|
||||
<input id="idState" class="form-control" type="text" name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state">
|
||||
[(ngModel)]="cipher.identity.state" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPostalCode">{{'zipPostalCode' | i18n}}</label>
|
||||
<input id="idPostalCode" class="form-control" type="text" name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode">
|
||||
[(ngModel)]="cipher.identity.postalCode" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCountry">{{'country' | i18n}}</label>
|
||||
<input id="idCountry" class="form-control" type="text" name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country">
|
||||
[(ngModel)]="cipher.identity.country" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{'notes' | i18n}}</label>
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes" [disabled]="cipher.isDeleted"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
<h3 class="mt-4">{{'customFields' | i18n}}</h3>
|
||||
@ -374,14 +381,14 @@
|
||||
</a>
|
||||
</div>
|
||||
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
|
||||
class="form-control" appInputVerbatim>
|
||||
class="form-control" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-7 form-group">
|
||||
<label for="fieldValue{{i}}">{{'value' | i18n}}</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="input-group" *ngIf="f.type === fieldType.Text">
|
||||
<input id="fieldValue{{i}}" class="form-control" type="text" name="Field.Value{{i}}"
|
||||
[(ngModel)]="f.value" appInputVerbatim>
|
||||
[(ngModel)]="f.value" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyValue' | i18n}}"
|
||||
@ -394,7 +401,7 @@
|
||||
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}"
|
||||
name="Field.Value{{i}}" [(ngModel)]="f.value"
|
||||
class="form-control text-monospace" appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
autocomplete="new-password" [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)"
|
||||
@ -414,24 +421,24 @@
|
||||
<div class="flex-fill">
|
||||
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
|
||||
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean" appTrueFalseValue
|
||||
trueValue="true" falseValue="false">
|
||||
trueValue="true" falseValue="false" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeField(f)"
|
||||
appA11yTitle="{{'remove' | i18n}}">
|
||||
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link text-muted cursor-move"
|
||||
appA11yTitle="{{'dragToSort' | i18n}}">
|
||||
appA11yTitle="{{'dragToSort' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-bars fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2">
|
||||
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newCustomField' | i18n}}
|
||||
</a>
|
||||
<div class="row">
|
||||
<div class="row" *ngIf="!cipher.isDeleted">
|
||||
<div class="col-5">
|
||||
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
|
||||
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
|
||||
@ -445,7 +452,8 @@
|
||||
<div class="col-5">
|
||||
<label for="organizationId">{{'whoOwnsThisItem' | i18n}}</label>
|
||||
<select id="organizationId" class="form-control" name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()">
|
||||
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -459,7 +467,7 @@
|
||||
<ng-container *ngIf="collections && collections.length">
|
||||
<div class="form-check" *ngFor="let c of collections; let i = index">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="c.checked"
|
||||
id="collection-{{i}}" name="Collection[{{i}}].Checked">
|
||||
id="collection-{{i}}" name="Collection[{{i}}].Checked" [disabled]="cipher.isDeleted">
|
||||
<label class="form-check-label" for="collection-{{i}}">{{c.name}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
@ -492,19 +500,20 @@
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
<span>{{(cipher.isDeleted ? 'restore' : 'save') | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto" *ngIf="cipher">
|
||||
<button *ngIf="!organization" type="button" (click)="toggleFavorite()" class="btn btn-link"
|
||||
<button *ngIf="!organization && !cipher.isDeleted" type="button" (click)="toggleFavorite()" class="btn btn-link"
|
||||
appA11yTitle="{{(cipher.favorite ? 'unfavorite' : 'favorite') | i18n}}">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-star': cipher.favorite, 'fa-star-o': !cipher.favorite}"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode && !cloneMode" [disabled]="deleteBtn.loading"
|
||||
appA11yTitle="{{(cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n}}"
|
||||
*ngIf="editMode && !cloneMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
@ -514,4 +523,4 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,19 +3,19 @@
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="deleteSelectedTitle">
|
||||
{{'deleteSelected' | i18n}}
|
||||
{{(permanent ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{'deleteSelectedItemsDesc' | i18n: cipherIds.length}}
|
||||
{{permanent ? 'permanentlyDeleteSelectedItemsDesc' : 'deleteSelectedItemsDesc' | i18n: cipherIds.length}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button appAutoFocus type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'delete' | i18n}}</span>
|
||||
<span>{{(permanent ? 'permanentlyDelete' : 'delete') | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
|
@ -17,6 +17,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
})
|
||||
export class BulkDeleteComponent {
|
||||
@Input() cipherIds: string[] = [];
|
||||
@Input() permanent: boolean = false;
|
||||
@Output() onDeleted = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ -25,10 +26,12 @@ export class BulkDeleteComponent {
|
||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.cipherService.deleteManyWithServer(this.cipherIds);
|
||||
this.formPromise = this.permanent ? this.cipherService.deleteManyWithServer(this.cipherIds) :
|
||||
this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
||||
await this.formPromise;
|
||||
this.onDeleted.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedItems'));
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
|
||||
: 'deletedItems'));
|
||||
}
|
||||
}
|
||||
|
25
src/app/vault/bulk-restore.component.html
Normal file
25
src/app/vault/bulk-restore.component.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="restoreSelectedTitle">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="restoreSelectedTitle">
|
||||
{{'restoreSelected' | i18n}}
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{'restoreSelectedItemsDesc' | i18n: cipherIds.length}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button appAutoFocus type="submit" class="btn btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'restore' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
34
src/app/vault/bulk-restore.component.ts
Normal file
34
src/app/vault/bulk-restore.component.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-bulk-restore',
|
||||
templateUrl: 'bulk-restore.component.html',
|
||||
})
|
||||
export class BulkRestoreComponent {
|
||||
@Input() cipherIds: string[] = [];
|
||||
@Output() onRestored = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private analytics: Angulartics2, private cipherService: CipherService,
|
||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.cipherService.restoreManyWithServer(this.cipherIds);
|
||||
await this.formPromise;
|
||||
this.onRestored.emit();
|
||||
this.analytics.eventTrack.next({ action: 'Bulk Restored Items' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('restoredItems'));
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
<td (click)="checkCipher(c)" class="reduced-lh wrap">
|
||||
<a href="#" appStopClick appStopProp (click)="selectCipher(c)"
|
||||
title="{{'editItem' | i18n}}">{{c.name}}</a>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<ng-container *ngIf="!organization && c.organizationId && !c.isDeleted">
|
||||
<i class="fa fa-share-alt" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'shared' | i18n}}</span>
|
||||
</ng-container>
|
||||
@ -36,7 +36,7 @@
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<ng-container *ngIf="c.type === cipherType.Login">
|
||||
<ng-container *ngIf="c.type === cipherType.Login && !c.isDeleted">
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
(click)="copy(c, c.login.password, 'password', 'password')">
|
||||
<i class="fa fa-fw fa-clipboard" aria-hidden="true"></i>
|
||||
@ -53,16 +53,18 @@
|
||||
{{'attachments' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
*ngIf="(!organization && !c.organizationId) || organization" (click)="clone(c)">
|
||||
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
|
||||
(click)="clone(c)">
|
||||
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
|
||||
{{'clone' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick *ngIf="!organization && !c.organizationId"
|
||||
<a class="dropdown-item" href="#" appStopClick
|
||||
*ngIf="!organization && !c.organizationId && !c.isDeleted"
|
||||
(click)="share(c)">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'share' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick *ngIf="c.organizationId"
|
||||
<a class="dropdown-item" href="#" appStopClick *ngIf="c.organizationId && !c.isDeleted"
|
||||
(click)="collections(c)">
|
||||
<i class="fa fa-fw fa-cubes" aria-hidden="true"></i>
|
||||
{{'collections' | i18n}}
|
||||
@ -72,9 +74,13 @@
|
||||
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="restore(c)" *ngIf="c.isDeleted">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'restore' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(c)">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'delete' | i18n}}
|
||||
{{(c.isDeleted ? 'permanentlyDelete' : 'delete') | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,4 +99,4 @@
|
||||
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -100,18 +100,43 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
const permanent = c.isDeleted;
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'),
|
||||
this.i18nService.t(permanent ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'),
|
||||
this.i18nService.t(permanent ? 'permanentlyDeleteItem' : 'deleteItem'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.deleteCipher(c.id);
|
||||
this.actionPromise = this.deleteCipher(c.id, permanent);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Cipher' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem'));
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t(permanent ? 'permanentlyDeletedItem'
|
||||
: 'deletedItem'));
|
||||
this.refresh();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async restore(c: CipherView): Promise<boolean> {
|
||||
if (this.actionPromise != null || !c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('restoreItemConfirmation'),
|
||||
this.i18nService.t('restoreItem'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.cipherService.restoreWithServer(c.id);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Restored Cipher' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('restoredItem'));
|
||||
this.refresh();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
@ -134,8 +159,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
||||
}
|
||||
}
|
||||
|
||||
protected deleteCipher(id: string) {
|
||||
return this.cipherService.deleteWithServer(id);
|
||||
protected deleteCipher(id: string, permanent: boolean) {
|
||||
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
|
@ -20,6 +20,11 @@
|
||||
<i class="fa-li fa fa-fw fa-star"></i>{{'favorites' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li [ngClass]="{active: selectedTrash}" *ngIf="showTrash">
|
||||
<a href="#" appStopClick (click)="selectTrash()">
|
||||
<i class="fa-li fa fa-fw fa-trash-o"></i>{{'trash' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{'types' | i18n}}</h3>
|
||||
<ul class="fa-ul card-ul">
|
||||
|
@ -4,7 +4,8 @@
|
||||
<app-vault-groupings (onAllClicked)="clearGroupingFilters()" (onFavoritesClicked)="filterFavorites()"
|
||||
(onCipherTypeClicked)="filterCipherType($event)" (onFolderClicked)="filterFolder($event.id)"
|
||||
(onAddFolder)="addFolder()" (onEditFolder)="editFolder($event.id)"
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)"
|
||||
(onTrashClicked)="filterDeleted()">
|
||||
</app-vault-groupings>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@ -27,17 +28,21 @@
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkMove()">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkMove()" *ngIf="!deleted">
|
||||
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
||||
{{'moveSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkShare()">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="bulkShare()" *ngIf="!deleted">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'shareSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" (click)="bulkRestore()" *ngIf="deleted">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'restoreSelected' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" (click)="bulkDelete()">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'deleteSelected' | i18n}}
|
||||
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(true)">
|
||||
@ -50,7 +55,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()" *ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
@ -118,6 +123,7 @@
|
||||
<ng-template #share></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #bulkDeleteTemplate></ng-template>
|
||||
<ng-template #bulkRestoreTemplate></ng-template>
|
||||
<ng-template #bulkMoveTemplate></ng-template>
|
||||
<ng-template #bulkShareTemplate></ng-template>
|
||||
<ng-template #updateKeyTemplate></ng-template>
|
||||
<ng-template #updateKeyTemplate></ng-template>
|
||||
|
@ -27,6 +27,7 @@ import { AddEditComponent } from './add-edit.component';
|
||||
import { AttachmentsComponent } from './attachments.component';
|
||||
import { BulkDeleteComponent } from './bulk-delete.component';
|
||||
import { BulkMoveComponent } from './bulk-move.component';
|
||||
import { BulkRestoreComponent } from './bulk-restore.component';
|
||||
import { BulkShareComponent } from './bulk-share.component';
|
||||
import { CiphersComponent } from './ciphers.component';
|
||||
import { CollectionsComponent } from './collections.component';
|
||||
@ -60,6 +61,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef;
|
||||
@ViewChild('updateKeyTemplate', { read: ViewContainerRef }) updateKeyModalRef: ViewContainerRef;
|
||||
@ -72,6 +74,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
showPremiumCallout = false;
|
||||
deleted: boolean = false;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
@ -104,7 +107,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.reload();
|
||||
} else {
|
||||
if (params.favorites) {
|
||||
if (params.deleted) {
|
||||
this.groupingsComponent.selectedTrash = true;
|
||||
await this.filterDeleted();
|
||||
} else if (params.favorites) {
|
||||
this.groupingsComponent.selectedFavorites = true;
|
||||
await this.filterFavorites();
|
||||
} else if (params.type) {
|
||||
@ -168,6 +174,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.go();
|
||||
}
|
||||
|
||||
async filterDeleted() {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.deleted = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchTrash');
|
||||
await this.ciphersComponent.reload(null, true);
|
||||
this.clearFilters();
|
||||
this.deleted = true;
|
||||
this.go();
|
||||
}
|
||||
|
||||
async filterCipherType(type: CipherType) {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
|
||||
@ -358,6 +374,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
@ -387,6 +407,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
|
||||
|
||||
childComponent.permanent = this.deleted;
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onDeleted.subscribe(async () => {
|
||||
this.modal.close();
|
||||
@ -398,6 +419,33 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
bulkRestore() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onRestored.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkShare() {
|
||||
const selectedCiphers = this.ciphersComponent.getSelected();
|
||||
if (selectedCiphers.length === 0) {
|
||||
@ -475,6 +523,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.collectionId = null;
|
||||
this.favorites = false;
|
||||
this.type = null;
|
||||
this.deleted = false;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
@ -484,6 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
type: this.type,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3053,5 +3053,87 @@
|
||||
"lock": {
|
||||
"message": "Lock",
|
||||
"description": "Verb form: to make secure or inaccesible by"
|
||||
},
|
||||
"trash": {
|
||||
"message": "Trash",
|
||||
"description": "Noun: A special folder for holding deleted items that have not yet been permanently deleted"
|
||||
},
|
||||
"searchTrash": {
|
||||
"message": "Search Trash"
|
||||
},
|
||||
"permanentlyDelete": {
|
||||
"message": "Permanently Delete"
|
||||
},
|
||||
"permanentlyDeleteSelected": {
|
||||
"message": "Permanently Delete Selected"
|
||||
},
|
||||
"permanentlyDeleteItem": {
|
||||
"message": "Permanently Delete Item"
|
||||
},
|
||||
"permanentlyDeleteItemConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this item?"
|
||||
},
|
||||
"permanentlyDeletedItem": {
|
||||
"message": "Permanently Deleted item"
|
||||
},
|
||||
"permanentlyDeletedItems": {
|
||||
"message": "Permanently Deleted items"
|
||||
},
|
||||
"permanentlyDeleteSelectedItemsDesc": {
|
||||
"message": "You have selected $COUNT$ item(s) to permanently delete. Are you sure you want to permanently delete all of these items?",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "150"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permanentlyDeletedItemId": {
|
||||
"message": "Permanently Deleted item $ID$.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
"example": "Google"
|
||||
}
|
||||
}
|
||||
},
|
||||
"restore": {
|
||||
"message": "Restore"
|
||||
},
|
||||
"restoreSelected": {
|
||||
"message": "Restore Selected"
|
||||
},
|
||||
"restoreItem": {
|
||||
"message": "Restore Item"
|
||||
},
|
||||
"restoredItem": {
|
||||
"message": "Restored Item"
|
||||
},
|
||||
"restoredItems": {
|
||||
"message": "Restored Items"
|
||||
},
|
||||
"restoreItemConfirmation": {
|
||||
"message": "Are you sure you want to restore this item?"
|
||||
},
|
||||
"restoreItems": {
|
||||
"message": "Restore items"
|
||||
},
|
||||
"restoreSelectedItemsDesc": {
|
||||
"message": "You have selected $COUNT$ item(s) to restore. Are you sure you want to restore all of these items?",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "150"
|
||||
}
|
||||
}
|
||||
},
|
||||
"restoredItemId": {
|
||||
"message": "Restored item $ID$.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
"example": "Google"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user