mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-17 15:37:57 +01:00
Allow Bulk Delete In Org Vault (#577)
* added the multi select checkbox to org ciphers * wired up select all/none * allowed for bulk delete of ciphers from the org vault * refactored bulk actions into a dedicated component * tweaked formatting settings and reformatted files * moved some shared code to jslib * some more formatting fixes * undid jslib connection changes * removed a function that was moved to jslib * reset jslib again? * set up delete many w/admin cipher methods * removed extra href tags * added organization id to bulk delete request model when coming from an org vault * fixed up some compiler warnings for formatting * code review fixups for bulk delete from org vault * added back a removed parameter from the vault component * seperated some imports with newlines * updated jslib * resolved some build errors * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * code review cleanup for bulk delete from an org vault * updated jslib to latest Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
This commit is contained in:
parent
49d5bfd3e7
commit
20408347fb
2
jslib
2
jslib
@ -1 +1 @@
|
|||||||
Subproject commit 7d49902eea45275d50c949beec32b3ab5b7db725
|
Subproject commit 420393700b38ed6e8e812366faf9231858bdaa92
|
@ -5,7 +5,10 @@ import {
|
|||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||||
|
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||||
|
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
import { StateService } from 'jslib/abstractions/state.service';
|
import { StateService } from 'jslib/abstractions/state.service';
|
||||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||||
@ -20,8 +23,13 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
constructor(authService: AuthService, router: Router,
|
constructor(authService: AuthService, router: Router,
|
||||||
i18nService: I18nService, private route: ActivatedRoute,
|
i18nService: I18nService, private route: ActivatedRoute,
|
||||||
storageService: StorageService, stateService: StateService,
|
storageService: StorageService, stateService: StateService,
|
||||||
platformUtilsService: PlatformUtilsService) {
|
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||||
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
|
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
|
||||||
|
super(authService, router,
|
||||||
|
platformUtilsService, i18nService,
|
||||||
|
stateService, environmentService,
|
||||||
|
passwordGenerationService, cryptoFunctionService,
|
||||||
|
storageService);
|
||||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +145,7 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
|
|||||||
|
|
||||||
import { AddEditComponent } from './vault/add-edit.component';
|
import { AddEditComponent } from './vault/add-edit.component';
|
||||||
import { AttachmentsComponent } from './vault/attachments.component';
|
import { AttachmentsComponent } from './vault/attachments.component';
|
||||||
|
import { BulkActionsComponent } from './vault/bulk-actions.component';
|
||||||
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
||||||
import { BulkMoveComponent } from './vault/bulk-move.component';
|
import { BulkMoveComponent } from './vault/bulk-move.component';
|
||||||
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
||||||
@ -260,6 +261,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
BlurClickDirective,
|
BlurClickDirective,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
BreachReportComponent,
|
BreachReportComponent,
|
||||||
|
BulkActionsComponent,
|
||||||
BulkDeleteComponent,
|
BulkDeleteComponent,
|
||||||
BulkMoveComponent,
|
BulkMoveComponent,
|
||||||
BulkRestoreComponent,
|
BulkRestoreComponent,
|
||||||
@ -381,6 +383,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
|||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
|
BulkActionsComponent,
|
||||||
BulkDeleteComponent,
|
BulkDeleteComponent,
|
||||||
BulkMoveComponent,
|
BulkMoveComponent,
|
||||||
BulkRestoreComponent,
|
BulkRestoreComponent,
|
||||||
|
@ -82,10 +82,6 @@ export class CiphersComponent extends BaseCiphersComponent {
|
|||||||
await this.resetPaging();
|
await this.resetPaging();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCipher(c: CipherView) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
events(c: CipherView) {
|
events(c: CipherView) {
|
||||||
this.onEventsClicked.emit(c);
|
this.onEventsClicked.emit(c);
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,15 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
|
<div class="ml-auto d-flex">
|
||||||
*ngIf="!deleted">
|
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted"
|
||||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
[organization]="organization">
|
||||||
</button>
|
</app-vault-bulk-actions>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
||||||
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||||
|
@ -53,7 +53,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
type: CipherType = null;
|
type: CipherType = null;
|
||||||
deleted: boolean = false;
|
deleted: boolean = false;
|
||||||
|
|
||||||
private modal: ModalComponent = null;
|
modal: ModalComponent = null;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
38
src/app/vault/bulk-actions.component.html
Normal file
38
src/app/vault/bulk-actions.component.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<div class="dropdown mr-2" appListDropdown>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||||
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkMove()" *ngIf="!deleted && !organization">
|
||||||
|
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
||||||
|
{{'moveSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkShare()" *ngIf="!deleted && !organization">
|
||||||
|
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||||
|
{{'shareSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
|
||||||
|
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||||
|
{{'restoreSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-danger" (click)="bulkDelete()">
|
||||||
|
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||||
|
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||||
|
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||||
|
{{'selectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||||
|
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||||
|
{{'unselectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #bulkDeleteTemplate></ng-template>
|
||||||
|
<ng-template #bulkRestoreTemplate></ng-template>
|
||||||
|
<ng-template #bulkMoveTemplate></ng-template>
|
||||||
|
<ng-template #bulkShareTemplate></ng-template>
|
154
src/app/vault/bulk-actions.component.ts
Normal file
154
src/app/vault/bulk-actions.component.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ComponentFactoryResolver,
|
||||||
|
Input,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
|
||||||
|
import { Organization } from 'jslib/models/domain/organization';
|
||||||
|
|
||||||
|
import { ModalComponent } from '../modal.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';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vault-bulk-actions',
|
||||||
|
templateUrl: 'bulk-actions.component.html',
|
||||||
|
})
|
||||||
|
export class BulkActionsComponent {
|
||||||
|
@Input() ciphersComponent: CiphersComponent;
|
||||||
|
@Input() modal: ModalComponent;
|
||||||
|
@Input() deleted: boolean;
|
||||||
|
@Input() organization: Organization;
|
||||||
|
|
||||||
|
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef;
|
||||||
|
|
||||||
|
constructor(private toasterService: ToasterService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||||
|
|
||||||
|
bulkDelete() {
|
||||||
|
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.bulkDeleteModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
|
||||||
|
|
||||||
|
childComponent.permanent = this.deleted;
|
||||||
|
childComponent.cipherIds = selectedIds;
|
||||||
|
childComponent.organization = this.organization;
|
||||||
|
childComponent.onDeleted.subscribe(async () => {
|
||||||
|
this.modal.close();
|
||||||
|
await this.ciphersComponent.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.onClosed.subscribe(() => {
|
||||||
|
this.modal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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.bulkShareModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
|
||||||
|
|
||||||
|
childComponent.ciphers = selectedCiphers;
|
||||||
|
childComponent.onShared.subscribe(async () => {
|
||||||
|
this.modal.close();
|
||||||
|
await this.ciphersComponent.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.onClosed.subscribe(async () => {
|
||||||
|
this.modal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkMove() {
|
||||||
|
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.bulkMoveModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
|
||||||
|
|
||||||
|
childComponent.cipherIds = selectedIds;
|
||||||
|
childComponent.onMoved.subscribe(async () => {
|
||||||
|
this.modal.close();
|
||||||
|
await this.ciphersComponent.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.onClosed.subscribe(() => {
|
||||||
|
this.modal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll(select: boolean) {
|
||||||
|
this.ciphersComponent.selectAll(select);
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,16 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
import { Angulartics2 } from 'angulartics2';
|
import { Angulartics2 } from 'angulartics2';
|
||||||
|
|
||||||
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
|
||||||
|
import { Organization } from 'jslib/models/domain/organization';
|
||||||
|
import { CipherBulkDeleteRequest } from 'jslib/models/request/cipherBulkDeleteRequest';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-vault-bulk-delete',
|
selector: 'app-vault-bulk-delete',
|
||||||
templateUrl: 'bulk-delete.component.html',
|
templateUrl: 'bulk-delete.component.html',
|
||||||
@ -18,20 +21,44 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
|||||||
export class BulkDeleteComponent {
|
export class BulkDeleteComponent {
|
||||||
@Input() cipherIds: string[] = [];
|
@Input() cipherIds: string[] = [];
|
||||||
@Input() permanent: boolean = false;
|
@Input() permanent: boolean = false;
|
||||||
|
@Input() organization: Organization;
|
||||||
@Output() onDeleted = new EventEmitter();
|
@Output() onDeleted = new EventEmitter();
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
constructor(private analytics: Angulartics2, private cipherService: CipherService,
|
constructor(private analytics: Angulartics2, private cipherService: CipherService,
|
||||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
private toasterService: ToasterService, private i18nService: I18nService,
|
||||||
|
private apiService: ApiService) { }
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.formPromise = this.permanent ? this.cipherService.deleteManyWithServer(this.cipherIds) :
|
if (!this.organization || !this.organization.isAdmin) {
|
||||||
this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
await this.deleteCiphers();
|
||||||
|
} else {
|
||||||
|
await this.deleteCiphersAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
|
|
||||||
this.onDeleted.emit();
|
this.onDeleted.emit();
|
||||||
this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' });
|
this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' });
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
|
this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
|
||||||
: 'deletedItems'));
|
: 'deletedItems'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async deleteCiphers() {
|
||||||
|
if (this.permanent) {
|
||||||
|
this.formPromise = await this.cipherService.deleteManyWithServer(this.cipherIds);
|
||||||
|
} else {
|
||||||
|
this.formPromise = await this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteCiphersAdmin() {
|
||||||
|
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
|
||||||
|
if (this.permanent) {
|
||||||
|
this.formPromise = await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
||||||
|
} else {
|
||||||
|
this.formPromise = await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of filteredCiphers">
|
<tr *ngFor="let c of filteredCiphers">
|
||||||
<td (click)="checkCipher(c)" class="table-list-checkbox" *ngIf="!organization">
|
<td (click)="checkCipher(c)" class="table-list-checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
|
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="checkCipher(c)" class="table-list-icon">
|
<td (click)="checkCipher(c)" class="table-list-icon">
|
||||||
@ -58,9 +58,8 @@
|
|||||||
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
|
||||||
{{'clone' | i18n}}
|
{{'clone' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href="#" appStopClick
|
<a class="dropdown-item" href="#" appStopClick
|
||||||
*ngIf="!organization && !c.organizationId && !c.isDeleted"
|
*ngIf="!organization && !c.organizationId && !c.isDeleted" (click)="share(c)">
|
||||||
(click)="share(c)">
|
|
||||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||||
{{'share' | i18n}}
|
{{'share' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -50,36 +50,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
|||||||
this.selectAll(false);
|
this.selectAll(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCipher(c: CipherView, select?: boolean) {
|
|
||||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
|
||||||
}
|
|
||||||
|
|
||||||
launch(uri: string) {
|
launch(uri: string) {
|
||||||
this.platformUtilsService.eventTrack('Launched Login URI');
|
this.platformUtilsService.eventTrack('Launched Login URI');
|
||||||
this.platformUtilsService.launchUri(uri);
|
this.platformUtilsService.launchUri(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAll(select: boolean) {
|
|
||||||
if (select) {
|
|
||||||
this.selectAll(false);
|
|
||||||
}
|
|
||||||
const selectCount = select && this.ciphers.length > MaxCheckedCount ? MaxCheckedCount : this.ciphers.length;
|
|
||||||
for (let i = 0; i < selectCount; i++) {
|
|
||||||
this.checkCipher(this.ciphers[i], select);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelected(): CipherView[] {
|
|
||||||
if (this.ciphers == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return this.ciphers.filter((c) => !!(c as any).checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedIds(): string[] {
|
|
||||||
return this.getSelected().map((c) => c.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
attachments(c: CipherView) {
|
attachments(c: CipherView) {
|
||||||
this.onAttachmentsClicked.emit(c);
|
this.onAttachmentsClicked.emit(c);
|
||||||
}
|
}
|
||||||
@ -159,6 +134,33 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectAll(select: boolean) {
|
||||||
|
if (select) {
|
||||||
|
this.selectAll(false);
|
||||||
|
}
|
||||||
|
const selectCount = select && this.ciphers.length > MaxCheckedCount
|
||||||
|
? MaxCheckedCount
|
||||||
|
: this.ciphers.length;
|
||||||
|
for (let i = 0; i < selectCount; i++) {
|
||||||
|
this.checkCipher(this.ciphers[i], select);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCipher(c: CipherView, select?: boolean) {
|
||||||
|
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelected(): CipherView[] {
|
||||||
|
if (this.ciphers == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.ciphers.filter((c) => !!(c as any).checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedIds(): string[] {
|
||||||
|
return this.getSelected().map((c) => c.id);
|
||||||
|
}
|
||||||
|
|
||||||
protected deleteCipher(id: string, permanent: boolean) {
|
protected deleteCipher(id: string, permanent: boolean) {
|
||||||
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
|
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
|
||||||
}
|
}
|
||||||
|
@ -21,40 +21,8 @@
|
|||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<div class="dropdown mr-2" appListDropdown>
|
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted">
|
||||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
</app-vault-bulk-actions>
|
||||||
id="bulkActionsButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
|
||||||
appA11yTitle="{{'options' | i18n}}">
|
|
||||||
<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()" *ngIf="!deleted">
|
|
||||||
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
|
||||||
{{'moveSelected' | i18n}}
|
|
||||||
</a>
|
|
||||||
<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>
|
|
||||||
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(true)">
|
|
||||||
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
|
||||||
{{'selectAll' | i18n}}
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(false)">
|
|
||||||
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
|
||||||
{{'unselectAll' | i18n}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()" *ngIf="!deleted">
|
<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}}
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
@ -122,8 +90,4 @@
|
|||||||
<ng-template #cipherAddEdit></ng-template>
|
<ng-template #cipherAddEdit></ng-template>
|
||||||
<ng-template #share></ng-template>
|
<ng-template #share></ng-template>
|
||||||
<ng-template #collections></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>
|
||||||
|
@ -13,8 +13,6 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { CipherType } from 'jslib/enums/cipherType';
|
import { CipherType } from 'jslib/enums/cipherType';
|
||||||
|
|
||||||
import { CipherView } from 'jslib/models/view/cipherView';
|
import { CipherView } from 'jslib/models/view/cipherView';
|
||||||
@ -25,10 +23,6 @@ import { OrganizationsComponent } from '../settings/organizations.component';
|
|||||||
import { UpdateKeyComponent } from '../settings/update-key.component';
|
import { UpdateKeyComponent } from '../settings/update-key.component';
|
||||||
import { AddEditComponent } from './add-edit.component';
|
import { AddEditComponent } from './add-edit.component';
|
||||||
import { AttachmentsComponent } from './attachments.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 { CiphersComponent } from './ciphers.component';
|
||||||
import { CollectionsComponent } from './collections.component';
|
import { CollectionsComponent } from './collections.component';
|
||||||
import { FolderAddEditComponent } from './folder-add-edit.component';
|
import { FolderAddEditComponent } from './folder-add-edit.component';
|
||||||
@ -60,10 +54,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||||
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
|
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
|
||||||
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: 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;
|
@ViewChild('updateKeyTemplate', { read: ViewContainerRef }) updateKeyModalRef: ViewContainerRef;
|
||||||
|
|
||||||
favorites: boolean = false;
|
favorites: boolean = false;
|
||||||
@ -76,15 +66,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
showPremiumCallout = false;
|
showPremiumCallout = false;
|
||||||
deleted: boolean = false;
|
deleted: boolean = false;
|
||||||
|
|
||||||
private modal: ModalComponent = null;
|
modal: ModalComponent = null;
|
||||||
|
|
||||||
constructor(private syncService: SyncService, private route: ActivatedRoute,
|
constructor(private syncService: SyncService, private route: ActivatedRoute,
|
||||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private tokenService: TokenService, private cryptoService: CryptoService,
|
private tokenService: TokenService, private cryptoService: CryptoService,
|
||||||
private messagingService: MessagingService, private userService: UserService,
|
private messagingService: MessagingService, private userService: UserService,
|
||||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
private platformUtilsService: PlatformUtilsService, private broadcasterService: BroadcasterService,
|
||||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
private ngZone: NgZone) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||||
@ -391,119 +381,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
component.cloneMode = true;
|
component.cloneMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bulkDelete() {
|
|
||||||
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.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();
|
|
||||||
await this.ciphersComponent.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modal.onClosed.subscribe(() => {
|
|
||||||
this.modal = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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.bulkShareModalRef.createComponent(factory).instance;
|
|
||||||
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
|
|
||||||
|
|
||||||
childComponent.ciphers = selectedCiphers;
|
|
||||||
childComponent.onShared.subscribe(async () => {
|
|
||||||
this.modal.close();
|
|
||||||
await this.ciphersComponent.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modal.onClosed.subscribe(async () => {
|
|
||||||
this.modal = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkMove() {
|
|
||||||
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.bulkMoveModalRef.createComponent(factory).instance;
|
|
||||||
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
|
|
||||||
|
|
||||||
childComponent.cipherIds = selectedIds;
|
|
||||||
childComponent.onMoved.subscribe(async () => {
|
|
||||||
this.modal.close();
|
|
||||||
await this.ciphersComponent.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modal.onClosed.subscribe(() => {
|
|
||||||
this.modal = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAll(select: boolean) {
|
|
||||||
this.ciphersComponent.selectAll(select);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateKey() {
|
updateKey() {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
Loading…
Reference in New Issue
Block a user