1
0
mirror of https://github.com/bitwarden/web synced 2026-01-03 09:03:41 +00:00

Apply Prettier (#1347)

This commit is contained in:
Oscar Hinton
2021-12-17 15:57:11 +01:00
committed by GitHub
parent 2b0a9d995e
commit 56477eb39c
414 changed files with 33390 additions and 26857 deletions

View File

@@ -1,96 +1,167 @@
<ng-container>
<h3 class="mt-4">{{'customFields' | i18n}}</h3>
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div class="row" cdkDrag *ngFor="let f of cipher.fields; let i = index; trackBy:trackByFunction">
<div class="col-5 form-group">
<div class="d-flex">
<label for="fieldName{{i}}">{{'name' | i18n}}</label>
<a class="ml-auto" href="https://help.bitwarden.com/article/custom-fields/"
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
class="form-control" appInputVerbatim [disabled]="cipher.isDeleted || viewOnly">
</div>
<div class="col-7 form-group">
<label for="fieldValue{{i}}">{{'value' | i18n}}</label>
<div class="d-flex align-items-center">
<!-- Text -->
<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
[disabled]="cipher.isDeleted || viewOnly">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', 'Field')">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Hidden -->
<div class="input-group" *ngIf="f.type === fieldType.Hidden">
<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"
[disabled]="cipher.isDeleted || viewOnly || (!cipher.viewPassword && !f.newField)">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)"
[disabled]="!cipher.viewPassword && !f.newField">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}">
</i>
</button>
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
[disabled]="!cipher.viewPassword && !f.newField">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Linked -->
<div class="input-group" *ngIf="f.type === fieldType.Linked">
<select id="fieldValue{{i}}" name="Field.Value{{i}}" class="form-control" [(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
[disabled]="cipher.isDeleted || viewOnly">
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="flex-fill">
<!-- Boolean -->
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean" appTrueFalseValue
trueValue="true" falseValue="false" [disabled]="cipher.isDeleted || viewOnly">
</div>
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeField(f)"
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted && !viewOnly">
<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}}" *ngIf="!cipher.isDeleted && !viewOnly">
<i class="fa fa-bars fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
<h3 class="mt-4">{{ "customFields" | i18n }}</h3>
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div
class="row"
cdkDrag
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
>
<div class="col-5 form-group">
<div class="d-flex">
<label for="fieldName{{ i }}">{{ "name" | i18n }}</label>
<a
class="ml-auto"
href="https://help.bitwarden.com/article/custom-fields/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
</div>
<!-- Add new custom field -->
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2"
*ngIf="!cipher.isDeleted && !viewOnly">
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newCustomField' | i18n}}
</a>
<div class="row" *ngIf="!cipher.isDeleted && !viewOnly">
<div class="col-5">
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option>
<option *ngIf="cipher.linkedFieldOptions != null" [ngValue]="addFieldLinkedTypeOption.value">
{{addFieldLinkedTypeOption.name}}
</option>
<input
id="fieldName{{ i }}"
type="text"
name="Field.Name{{ i }}"
[(ngModel)]="f.name"
class="form-control"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
/>
</div>
<div class="col-7 form-group">
<label for="fieldValue{{ i }}">{{ "value" | i18n }}</label>
<div class="d-flex align-items-center">
<!-- Text -->
<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
[disabled]="cipher.isDeleted || viewOnly"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(f.value, 'value', 'Field')"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Hidden -->
<div class="input-group" *ngIf="f.type === fieldType.Hidden">
<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"
[disabled]="cipher.isDeleted || viewOnly || (!cipher.viewPassword && !f.newField)"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleFieldValue(f)"
[disabled]="!cipher.viewPassword && !f.newField"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue }"
>
</i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
[disabled]="!cipher.viewPassword && !f.newField"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Linked -->
<div class="input-group" *ngIf="f.type === fieldType.Linked">
<select
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
class="form-control"
[(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="flex-fill">
<!-- Boolean -->
<input
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
type="checkbox"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean"
appTrueFalseValue
trueValue="true"
falseValue="false"
[disabled]="cipher.isDeleted || viewOnly"
/>
</div>
<button
type="button"
class="btn btn-link text-danger ml-2"
(click)="removeField(f)"
appA11yTitle="{{ 'remove' | i18n }}"
*ngIf="!cipher.isDeleted && !viewOnly"
>
<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 }}"
*ngIf="!cipher.isDeleted && !viewOnly"
>
<i class="fa fa-bars fa-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Add new custom field -->
<a
href="#"
appStopClick
(click)="addField()"
class="d-inline-block mb-2"
*ngIf="!cipher.isDeleted && !viewOnly"
>
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{ "newCustomField" | i18n }}
</a>
<div class="row" *ngIf="!cipher.isDeleted && !viewOnly">
<div class="col-5">
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option
*ngIf="cipher.linkedFieldOptions != null"
[ngValue]="addFieldLinkedTypeOption.value"
>
{{ addFieldLinkedTypeOption.name }}
</option>
</select>
</div>
</div>
</ng-container>

View File

@@ -1,24 +1,19 @@
import {
Component,
Input,
} from '@angular/core';
import { Component, Input } from "@angular/core";
import {
AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent
} from 'jslib-angular/components/add-edit-custom-fields.component';
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "jslib-angular/components/add-edit-custom-fields.component";
import { EventService } from 'jslib-common/abstractions/event.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Component({
selector: 'app-vault-add-edit-custom-fields',
templateUrl: 'add-edit-custom-fields.component.html',
selector: "app-vault-add-edit-custom-fields",
templateUrl: "add-edit-custom-fields.component.html",
})
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
@Input() viewOnly: boolean;
@Input() copy: (value: string, typeI18nKey: string, aType: string) => void;
@Input() viewOnly: boolean;
@Input() copy: (value: string, typeI18nKey: string, aType: string) => void;
constructor(i18nService: I18nService, eventService: EventService) {
super(i18nService, eventService);
}
constructor(i18nService: I18nService, eventService: EventService) {
super(i18nService, eventService);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +1,215 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { CipherType } from 'jslib-common/enums/cipherType';
import { EventType } from 'jslib-common/enums/eventType';
import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from "jslib-common/enums/eventType";
import { AuditService } from 'jslib-common/abstractions/audit.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { TotpService } from 'jslib-common/abstractions/totp.service';
import { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/components/add-edit.component';
import { LoginUriView } from 'jslib-common/models/view/loginUriView';
import { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/add-edit.component";
import { LoginUriView } from "jslib-common/models/view/loginUriView";
@Component({
selector: 'app-vault-add-edit',
templateUrl: 'add-edit.component.html',
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent {
canAccessPremium: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
totpSec: number;
totpLow: boolean;
showRevisionDate = false;
hasPasswordHistory = false;
viewingPasswordHistory = false;
viewOnly = false;
canAccessPremium: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
totpSec: number;
totpLow: boolean;
showRevisionDate = false;
hasPasswordHistory = false;
viewingPasswordHistory = false;
viewOnly = false;
protected totpInterval: number;
protected totpInterval: number;
constructor(cipherService: CipherService, folderService: FolderService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
auditService: AuditService, stateService: StateService,
collectionService: CollectionService, protected totpService: TotpService,
protected passwordGenerationService: PasswordGenerationService, protected messagingService: MessagingService,
eventService: EventService, protected policyService: PolicyService, organizationService: OrganizationService, logService: LogService,
passwordRepromptService: PasswordRepromptService) {
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
collectionService, messagingService, eventService, policyService, logService, passwordRepromptService, organizationService);
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
stateService: StateService,
collectionService: CollectionService,
protected totpService: TotpService,
protected passwordGenerationService: PasswordGenerationService,
protected messagingService: MessagingService,
eventService: EventService,
protected policyService: PolicyService,
organizationService: OrganizationService,
logService: LogService,
passwordRepromptService: PasswordRepromptService
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
stateService,
collectionService,
messagingService,
eventService,
policyService,
logService,
passwordRepromptService,
organizationService
);
}
async ngOnInit() {
await super.ngOnInit();
await this.load();
this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null;
this.hasPasswordHistory = this.cipher.hasPasswordHistory;
this.cleanUp();
this.canAccessPremium = await this.stateService.getCanAccessPremium();
if (
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
(this.cipher.organizationUseTotp || this.canAccessPremium)
) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
this.totpInterval = window.setInterval(async () => {
await this.totpTick(interval);
}, 1000);
}
}
toggleFavorite() {
this.cipher.favorite = !this.cipher.favorite;
}
launch(uri: LoginUriView) {
if (!uri.canLaunch) {
return;
}
async ngOnInit() {
await super.ngOnInit();
await this.load();
this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null;
this.hasPasswordHistory = this.cipher.hasPasswordHistory;
this.cleanUp();
this.platformUtilsService.launchUri(uri.launchUri);
}
this.canAccessPremium = await this.stateService.getCanAccessPremium();
if (this.cipher.type === CipherType.Login && this.cipher.login.totp &&
(this.cipher.organizationUseTotp || this.canAccessPremium)) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
this.totpInterval = window.setInterval(async () => {
await this.totpTick(interval);
}, 1000);
}
copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}
toggleFavorite() {
this.cipher.favorite = !this.cipher.favorite;
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
if (this.editMode) {
if (typeI18nKey === "password") {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
} else if (typeI18nKey === "securityCode") {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
} else if (aType === "H_Field") {
this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
}
}
}
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
const options = (await this.passwordGenerationService.getOptions())[0];
this.cipher.login.password = await this.passwordGenerationService.generatePassword(options);
}
return confirmed;
}
premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
upgradeOrganization() {
this.messagingService.send("upgradeOrganization", {
organizationId: this.cipher.organizationId,
});
}
viewHistory() {
this.viewingPasswordHistory = !this.viewingPasswordHistory;
}
protected cleanUp() {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
protected async totpUpdateCode() {
if (
this.cipher == null ||
this.cipher.type !== CipherType.Login ||
this.cipher.login.totp == null
) {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
return;
}
launch(uri: LoginUriView) {
if (!uri.canLaunch) {
return;
}
this.platformUtilsService.launchUri(uri.launchUri);
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
if (this.totpCode != null) {
if (this.totpCode.length > 4) {
const half = Math.floor(this.totpCode.length / 2);
this.totpCodeFormatted =
this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
} else {
this.totpCodeFormatted = this.totpCode;
}
} else {
this.totpCodeFormatted = null;
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
}
copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}
protected allowOwnershipAssignment() {
return (
(!this.editMode || this.cloneMode) &&
this.ownershipOptions != null &&
(this.ownershipOptions.length > 1 || !this.allowPersonal)
);
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
private async totpTick(intervalSeconds: number) {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % intervalSeconds;
if (this.editMode) {
if (typeI18nKey === 'password') {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
} else if (typeI18nKey === 'securityCode') {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
} else if (aType === 'H_Field') {
this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
}
}
}
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
const options = (await this.passwordGenerationService.getOptions())[0];
this.cipher.login.password = await this.passwordGenerationService.generatePassword(options);
}
return confirmed;
}
premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
}
}
upgradeOrganization() {
this.messagingService.send('upgradeOrganization', { organizationId: this.cipher.organizationId });
}
viewHistory() {
this.viewingPasswordHistory = !this.viewingPasswordHistory;
}
protected cleanUp() {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
protected async totpUpdateCode() {
if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
return;
}
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
if (this.totpCode != null) {
if (this.totpCode.length > 4) {
const half = Math.floor(this.totpCode.length / 2);
this.totpCodeFormatted = this.totpCode.substring(0, half) + ' ' + this.totpCode.substring(half);
} else {
this.totpCodeFormatted = this.totpCode;
}
} else {
this.totpCodeFormatted = null;
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
}
protected allowOwnershipAssignment() {
return (!this.editMode || this.cloneMode) && this.ownershipOptions != null
&& (this.ownershipOptions.length > 1 || !this.allowPersonal);
}
private async totpTick(intervalSeconds: number) {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % intervalSeconds;
this.totpSec = intervalSeconds - mod;
this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2');
this.totpLow = this.totpSec <= 7;
if (mod === 0) {
await this.totpUpdateCode();
}
this.totpSec = intervalSeconds - mod;
this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2");
this.totpLow = this.totpSec <= 7;
if (mod === 0) {
await this.totpUpdateCode();
}
}
}

View File

@@ -1,68 +1,116 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="attachmentsTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="attachmentsTitle">
{{'attachments' | i18n}}
<small *ngIf="cipher">{{cipher.name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-hover table-list" *ngIf="cipher && cipher.hasAttachments">
<tbody>
<tr *ngFor="let a of cipher.attachments">
<td class="table-list-icon">
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="!a.downloading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-lg fa-fw fa-spin" *ngIf="a.downloading"
aria-hidden="true"></i>
</td>
<td class="wrap">
<div class="d-flex">
<a href="#" appStopClick (click)="download(a)">{{a.fileName}}</a>
<div *ngIf="showFixOldAttachments(a)" class="ml-2">
<a href="https://help.bitwarden.com/article/attachments/#fixing-old-attachments"
target="_blank" rel="noopener">
<i class="fa fa-exclamation-triangle text-warning"
title="{{'attachmentFixDesc' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'attachmentFixDesc' | i18n}}</span></a>
<button type="button" class="btn btn-outline-primary btn-sm m-0 py-0 px-2"
(click)="reupload(a)" #reuploadBtn [appApiAction]="reuploadPromises[a.id]"
[disabled]="reuploadBtn.loading">{{'fix' | i18n}}</button>
</div>
</div>
<small>{{a.sizeName}}</small>
</td>
<td class="table-list-options" *ngIf="!viewOnly">
<button class="btn btn-outline-danger" type="button" appStopClick
appA11yTitle="{{'delete' | i18n}}" (click)="delete(a)" #deleteBtn
[appApiAction]="deletePromises[a.id]" [disabled]="deleteBtn.loading">
<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"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</td>
</tr>
</tbody>
</table>
<div *ngIf="!viewOnly">
<h3>{{'newAttachment' | i18n}}</h3>
<label for="file" class="sr-only">{{'file' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="attachmentsTitle">
{{ "attachments" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-hover table-list" *ngIf="cipher && cipher.hasAttachments">
<tbody>
<tr *ngFor="let a of cipher.attachments">
<td class="table-list-icon">
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="!a.downloading" aria-hidden="true"></i>
<i
class="fa fa-spinner fa-lg fa-fw fa-spin"
*ngIf="a.downloading"
aria-hidden="true"
></i>
</td>
<td class="wrap">
<div class="d-flex">
<a href="#" appStopClick (click)="download(a)">{{ a.fileName }}</a>
<div *ngIf="showFixOldAttachments(a)" class="ml-2">
<a
href="https://help.bitwarden.com/article/attachments/#fixing-old-attachments"
target="_blank"
rel="noopener"
>
<i
class="fa fa-exclamation-triangle text-warning"
title="{{ 'attachmentFixDesc' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachmentFixDesc" | i18n }}</span></a
>
<button
type="button"
class="btn btn-outline-primary btn-sm m-0 py-0 px-2"
(click)="reupload(a)"
#reuploadBtn
[appApiAction]="reuploadPromises[a.id]"
[disabled]="reuploadBtn.loading"
>
{{ "fix" | i18n }}
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="!viewOnly">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
<small>{{ a.sizeName }}</small>
</td>
<td class="table-list-options" *ngIf="!viewOnly">
<button
class="btn btn-outline-danger"
type="button"
appStopClick
appA11yTitle="{{ 'delete' | i18n }}"
(click)="delete(a)"
#deleteBtn
[appApiAction]="deletePromises[a.id]"
[disabled]="deleteBtn.loading"
>
<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"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close'
| i18n}}</button>
</div>
</form>
</div>
</td>
</tr>
</tbody>
</table>
<div *ngIf="!viewOnly">
<h3>{{ "newAttachment" | i18n }}</h3>
<label for="file" class="sr-only">{{ "file" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{ "maxFileSize" | i18n }}</small>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading"
*ngIf="!viewOnly"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,38 +1,52 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { AttachmentView } from 'jslib-common/models/view/attachmentView';
import { AttachmentView } from "jslib-common/models/view/attachmentView";
import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/components/attachments.component';
import { AttachmentsComponent as BaseAttachmentsComponent } from "jslib-angular/components/attachments.component";
@Component({
selector: 'app-vault-attachments',
templateUrl: 'attachments.component.html',
selector: "app-vault-attachments",
templateUrl: "attachments.component.html",
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = false;
viewOnly = false;
constructor(cipherService: CipherService, i18nService: I18nService,
cryptoService: CryptoService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService, logService: LogService) {
super(cipherService, i18nService, cryptoService, platformUtilsService, apiService, window, logService,
stateService);
}
constructor(
cipherService: CipherService,
i18nService: I18nService,
cryptoService: CryptoService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService
);
}
protected async reupload(attachment: AttachmentView) {
if (this.showFixOldAttachments(attachment)) {
await this.reuploadCipherAttachment(attachment, false);
}
protected async reupload(attachment: AttachmentView) {
if (this.showFixOldAttachments(attachment)) {
await this.reuploadCipherAttachment(attachment, false);
}
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.cipher.organizationId == null;
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.cipher.organizationId == null;
}
}

View File

@@ -1,35 +1,52 @@
<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
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>
<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-arrow-circle-o-right" aria-hidden="true"></i>
{{'moveSelectedToOrg' | 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>
<button
class="dropdown-item"
appStopClick
(click)="bulkShare()"
*ngIf="!deleted && !organization"
>
<i class="fa fa-fw fa-arrow-circle-o-right" aria-hidden="true"></i>
{{ "moveSelectedToOrg" | 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>

View File

@@ -1,136 +1,169 @@
import {
Component,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Component, Input, ViewChild, ViewContainerRef } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization";
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 { 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',
selector: "app-vault-bulk-actions",
templateUrl: "bulk-actions.component.html",
})
export class BulkActionsComponent {
@Input() ciphersComponent: CiphersComponent;
@Input() deleted: boolean;
@Input() organization: Organization;
@Input() ciphersComponent: CiphersComponent;
@Input() deleted: boolean;
@Input() organization: Organization;
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef, static: true }) bulkDeleteModalRef: ViewContainerRef;
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef, static: true }) bulkRestoreModalRef: ViewContainerRef;
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef, static: true }) bulkMoveModalRef: ViewContainerRef;
@ViewChild('bulkShareTemplate', { read: ViewContainerRef, static: true }) bulkShareModalRef: ViewContainerRef;
@ViewChild("bulkDeleteTemplate", { read: ViewContainerRef, static: true })
bulkDeleteModalRef: ViewContainerRef;
@ViewChild("bulkRestoreTemplate", { read: ViewContainerRef, static: true })
bulkRestoreModalRef: ViewContainerRef;
@ViewChild("bulkMoveTemplate", { read: ViewContainerRef, static: true })
bulkMoveModalRef: ViewContainerRef;
@ViewChild("bulkShareTemplate", { read: ViewContainerRef, static: true })
bulkShareModalRef: ViewContainerRef;
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private modalService: ModalService, private passwordRepromptService: PasswordRepromptService) { }
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private modalService: ModalService,
private passwordRepromptService: PasswordRepromptService
) {}
async bulkDelete() {
if (!await this.promptPassword()) {
return;
}
async bulkDelete() {
if (!(await this.promptPassword())) {
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const [modal] = await this.modalService.openViewRef(BulkDeleteComponent, this.bulkDeleteModalRef, comp => {
comp.permanent = this.deleted;
comp.cipherIds = selectedIds;
comp.organization = this.organization;
comp.onDeleted.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
const [modal] = await this.modalService.openViewRef(
BulkDeleteComponent,
this.bulkDeleteModalRef,
(comp) => {
comp.permanent = this.deleted;
comp.cipherIds = selectedIds;
comp.organization = this.organization;
comp.onDeleted.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async bulkRestore() {
if (!(await this.promptPassword())) {
return;
}
async bulkRestore() {
if (!await this.promptPassword()) {
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
const [modal] = await this.modalService.openViewRef(BulkRestoreComponent, this.bulkRestoreModalRef, comp => {
comp.cipherIds = selectedIds;
comp.onRestored.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
const [modal] = await this.modalService.openViewRef(
BulkRestoreComponent,
this.bulkRestoreModalRef,
(comp) => {
comp.cipherIds = selectedIds;
comp.onRestored.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async bulkShare() {
if (!(await this.promptPassword())) {
return;
}
async bulkShare() {
if (!await this.promptPassword()) {
return;
}
const selectedCiphers = this.ciphersComponent.getSelected();
if (selectedCiphers.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const selectedCiphers = this.ciphersComponent.getSelected();
if (selectedCiphers.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
const [modal] = await this.modalService.openViewRef(BulkShareComponent, this.bulkShareModalRef, comp => {
comp.ciphers = selectedCiphers;
comp.onShared.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
const [modal] = await this.modalService.openViewRef(
BulkShareComponent,
this.bulkShareModalRef,
(comp) => {
comp.ciphers = selectedCiphers;
comp.onShared.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async bulkMove() {
if (!(await this.promptPassword())) {
return;
}
async bulkMove() {
if (!await this.promptPassword()) {
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
const [modal] = await this.modalService.openViewRef(BulkMoveComponent, this.bulkMoveModalRef, comp => {
comp.cipherIds = selectedIds;
comp.onMoved.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
const [modal] = await this.modalService.openViewRef(
BulkMoveComponent,
this.bulkMoveModalRef,
(comp) => {
comp.cipherIds = selectedIds;
comp.onMoved.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
}
);
}
selectAll(select: boolean) {
this.ciphersComponent.selectAll(select);
}
selectAll(select: boolean) {
this.ciphersComponent.selectAll(select);
}
private async promptPassword() {
const selectedCiphers = this.ciphersComponent.getSelected();
const notProtected = !selectedCiphers.find(cipher => cipher.reprompt !== CipherRepromptType.None);
private async promptPassword() {
const selectedCiphers = this.ciphersComponent.getSelected();
const notProtected = !selectedCiphers.find(
(cipher) => cipher.reprompt !== CipherRepromptType.None
);
return notProtected || await this.passwordRepromptService.showPasswordPrompt();
}
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
}
}

View File

@@ -1,25 +1,39 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deleteSelectedTitle">
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="deleteSelectedTitle">
{{(permanent ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{(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>{{(permanent ? 'permanentlyDelete' : 'delete') | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="deleteSelectedTitle">
{{ (permanent ? "permanentlyDeleteSelected" : "deleteSelected") | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{
(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>{{ (permanent ? "permanentlyDelete" : "delete") | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,61 +1,63 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { CipherBulkDeleteRequest } from 'jslib-common/models/request/cipherBulkDeleteRequest';
import { Organization } from "jslib-common/models/domain/organization";
import { CipherBulkDeleteRequest } from "jslib-common/models/request/cipherBulkDeleteRequest";
@Component({
selector: 'app-vault-bulk-delete',
templateUrl: 'bulk-delete.component.html',
selector: "app-vault-bulk-delete",
templateUrl: "bulk-delete.component.html",
})
export class BulkDeleteComponent {
@Input() cipherIds: string[] = [];
@Input() permanent: boolean = false;
@Input() organization: Organization;
@Output() onDeleted = new EventEmitter();
@Input() cipherIds: string[] = [];
@Input() permanent: boolean = false;
@Input() organization: Organization;
@Output() onDeleted = new EventEmitter();
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(private cipherService: CipherService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private apiService: ApiService) { }
constructor(
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private apiService: ApiService
) {}
async submit() {
if (!this.organization || !this.organization.canEditAnyCollection) {
await this.deleteCiphers();
} else {
await this.deleteCiphersAdmin();
}
await this.formPromise;
this.onDeleted.emit();
this.platformUtilsService.showToast('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
: 'deletedItems'));
async submit() {
if (!this.organization || !this.organization.canEditAnyCollection) {
await this.deleteCiphers();
} else {
await this.deleteCiphersAdmin();
}
private async deleteCiphers() {
if (this.permanent) {
this.formPromise = await this.cipherService.deleteManyWithServer(this.cipherIds);
} else {
this.formPromise = await this.cipherService.softDeleteManyWithServer(this.cipherIds);
}
}
await this.formPromise;
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);
}
this.onDeleted.emit();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "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);
}
}
}

View File

@@ -1,31 +1,37 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="moveSelectedTitle">
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="moveSelectedTitle">
{{'moveSelected' | i18n}}
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'moveSelectedItemsDesc' | i18n: cipherIds.length}}</p>
<div class="form-group">
<label for="folder">{{'folder' | i18n}}</label>
<select id="folder" name="FolderId" [(ngModel)]="folderId" class="form-control">
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
</select>
</div>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="moveSelectedTitle">
{{ "moveSelected" | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
<div class="form-group">
<label for="folder">{{ "folder" | i18n }}</label>
<select id="folder" name="FolderId" [(ngModel)]="folderId" class="form-control">
<option *ngFor="let f of folders" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,42 +1,40 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { FolderView } from 'jslib-common/models/view/folderView';
import { FolderView } from "jslib-common/models/view/folderView";
@Component({
selector: 'app-vault-bulk-move',
templateUrl: 'bulk-move.component.html',
selector: "app-vault-bulk-move",
templateUrl: "bulk-move.component.html",
})
export class BulkMoveComponent implements OnInit {
@Input() cipherIds: string[] = [];
@Output() onMoved = new EventEmitter();
@Input() cipherIds: string[] = [];
@Output() onMoved = new EventEmitter();
folderId: string = null;
folders: FolderView[] = [];
formPromise: Promise<any>;
folderId: string = null;
folders: FolderView[] = [];
formPromise: Promise<any>;
constructor(private cipherService: CipherService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private folderService: FolderService) { }
constructor(
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private folderService: FolderService
) {}
async ngOnInit() {
this.folders = await this.folderService.getAllDecrypted();
this.folderId = this.folders[0].id;
}
async ngOnInit() {
this.folders = await this.folderService.getAllDecrypted();
this.folderId = this.folders[0].id;
}
async submit() {
this.formPromise = this.cipherService.moveManyWithServer(this.cipherIds, this.folderId);
await this.formPromise;
this.onMoved.emit();
this.platformUtilsService.showToast('success', null, this.i18nService.t('movedItems'));
}
async submit() {
this.formPromise = this.cipherService.moveManyWithServer(this.cipherIds, this.folderId);
await this.formPromise;
this.onMoved.emit();
this.platformUtilsService.showToast("success", null, this.i18nService.t("movedItems"));
}
}

View File

@@ -1,25 +1,36 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="restoreSelectedTitle">
<div class="modal-dialog modal-dialog-scrollable 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">&times;</span>
</button>
</div>
<div class="modal-body">
{{'restoreSelectedItemsDesc' | i18n: cipherIds.length}}
</div>
<div class="modal-footer">
<button appAutoFocus 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>{{'restore' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable 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">&times;</span>
</button>
</div>
<div class="modal-body">
{{ "restoreSelectedItemsDesc" | i18n: cipherIds.length }}
</div>
<div class="modal-footer">
<button
appAutoFocus
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>{{ "restore" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,31 +1,29 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: 'app-vault-bulk-restore',
templateUrl: 'bulk-restore.component.html',
selector: "app-vault-bulk-restore",
templateUrl: "bulk-restore.component.html",
})
export class BulkRestoreComponent {
@Input() cipherIds: string[] = [];
@Output() onRestored = new EventEmitter();
@Input() cipherIds: string[] = [];
@Output() onRestored = new EventEmitter();
formPromise: Promise<any>;
formPromise: Promise<any>;
constructor(private cipherService: CipherService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService) { }
constructor(
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
async submit() {
this.formPromise = this.cipherService.restoreManyWithServer(this.cipherIds);
await this.formPromise;
this.onRestored.emit();
this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItems'));
}
async submit() {
this.formPromise = this.cipherService.restoreManyWithServer(this.cipherIds);
await this.formPromise;
this.onRestored.emit();
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
}
}

View File

@@ -1,62 +1,85 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="moveSelectedToOrgTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="moveSelectedToOrgTitle">
{{'moveSelectedToOrg' | i18n}}
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'moveManyToOrgDesc' | i18n}}</p>
<p>{{'moveSelectedItemsCountDesc' | i18n: this.ciphers.length : shareableCiphers.length : nonShareableCount}}
</p>
<div class="form-group">
<label for="organization">{{'organization' | i18n}}</label>
<select id="organization" name="OrganizationId" [(ngModel)]="organizationId" class="form-control"
(change)="filterCollections()">
<option *ngFor="let o of organizations" [ngValue]="o.id">{{o.name}}</option>
</select>
</div>
<div class="d-flex">
<h3>{{'collections' | i18n}}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{'noCollectionsInList' | i18n}}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
appStopProp>
</td>
<td>
{{c.name}}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit manual" [disabled]="form.loading || !canSave"
[ngClass]="{loading:form.loading}">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="moveSelectedToOrgTitle">
{{ "moveSelectedToOrg" | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "moveManyToOrgDesc" | i18n }}</p>
<p>
{{
"moveSelectedItemsCountDesc"
| i18n: this.ciphers.length:shareableCiphers.length:nonShareableCount
}}
</p>
<div class="form-group">
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
class="form-control"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[disabled]="form.loading || !canSave"
[ngClass]="{ loading: form.loading }"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,100 +1,118 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization";
@Component({
selector: 'app-vault-bulk-share',
templateUrl: 'bulk-share.component.html',
selector: "app-vault-bulk-share",
templateUrl: "bulk-share.component.html",
})
export class BulkShareComponent implements OnInit {
@Input() ciphers: CipherView[] = [];
@Input() organizationId: string;
@Output() onShared = new EventEmitter();
@Input() ciphers: CipherView[] = [];
@Input() organizationId: string;
@Output() onShared = new EventEmitter();
nonShareableCount = 0;
collections: CollectionView[] = [];
organizations: Organization[] = [];
shareableCiphers: CipherView[] = [];
formPromise: Promise<any>;
nonShareableCount = 0;
collections: CollectionView[] = [];
organizations: Organization[] = [];
shareableCiphers: CipherView[] = [];
formPromise: Promise<any>;
private writeableCollections: CollectionView[] = [];
private writeableCollections: CollectionView[] = [];
constructor(private cipherService: CipherService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private collectionService: CollectionService,
private organizationService: OrganizationService, private logService: LogService) { }
constructor(
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private logService: LogService
) {}
async ngOnInit() {
this.shareableCiphers = this.ciphers.filter(c => !c.hasOldAttachments && c.organizationId == null);
this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length;
const allCollections = await this.collectionService.getAllDecrypted();
this.writeableCollections = allCollections.filter(c => !c.readOnly);
this.organizations = await this.organizationService.getAll();
if (this.organizationId == null && this.organizations.length > 0) {
this.organizationId = this.organizations[0].id;
async ngOnInit() {
this.shareableCiphers = this.ciphers.filter(
(c) => !c.hasOldAttachments && c.organizationId == null
);
this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length;
const allCollections = await this.collectionService.getAllDecrypted();
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
this.organizations = await this.organizationService.getAll();
if (this.organizationId == null && this.organizations.length > 0) {
this.organizationId = this.organizations[0].id;
}
this.filterCollections();
}
ngOnDestroy() {
this.selectAll(false);
}
filterCollections() {
this.selectAll(false);
if (this.organizationId == null || this.writeableCollections.length === 0) {
this.collections = [];
} else {
this.collections = this.writeableCollections.filter(
(c) => c.organizationId === this.organizationId
);
}
}
async submit() {
const checkedCollectionIds = this.collections
.filter((c) => (c as any).checked)
.map((c) => c.id);
try {
this.formPromise = this.cipherService.shareManyWithServer(
this.shareableCiphers,
this.organizationId,
checkedCollectionIds
);
await this.formPromise;
this.onShared.emit();
const orgName =
this.organizations.find((o) => o.id === this.organizationId)?.name ??
this.i18nService.t("organization");
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("movedItemsToOrg", orgName)
);
} catch (e) {
this.logService.error(e);
}
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach((c) => this.check(c, select));
}
get canSave() {
if (
this.shareableCiphers != null &&
this.shareableCiphers.length > 0 &&
this.collections != null
) {
for (let i = 0; i < this.collections.length; i++) {
if ((this.collections[i] as any).checked) {
return true;
}
this.filterCollections();
}
ngOnDestroy() {
this.selectAll(false);
}
filterCollections() {
this.selectAll(false);
if (this.organizationId == null || this.writeableCollections.length === 0) {
this.collections = [];
} else {
this.collections = this.writeableCollections.filter(c => c.organizationId === this.organizationId);
}
}
async submit() {
const checkedCollectionIds = this.collections.filter(c => (c as any).checked).map(c => c.id);
try {
this.formPromise = this.cipherService.shareManyWithServer(this.shareableCiphers, this.organizationId,
checkedCollectionIds);
await this.formPromise;
this.onShared.emit();
const orgName = this.organizations.find(o => o.id === this.organizationId)?.name ?? this.i18nService.t('organization');
this.platformUtilsService.showToast('success', null, this.i18nService.t('movedItemsToOrg', orgName));
} catch (e) {
this.logService.error(e);
}
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach(c => this.check(c, select));
}
get canSave() {
if (this.shareableCiphers != null && this.shareableCiphers.length > 0 && this.collections != null) {
for (let i = 0; i < this.collections.length; i++) {
if ((this.collections[i] as any).checked) {
return true;
}
}
}
return false;
}
}
return false;
}
}

View File

@@ -1,111 +1,187 @@
<ng-container *ngIf="(isPaging() ? pagedCiphers : ciphers) as filteredCiphers">
<table class="table table-hover table-list table-ciphers" *ngIf="filteredCiphers.length" infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let c of filteredCiphers">
<td (click)="checkCipher(c)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
</td>
<td (click)="checkCipher(c)" class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<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">
<i class="fa fa-cube" appStopProp title="{{'shared' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'shared' | i18n}}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i class="fa fa-paperclip" appStopProp title="{{'attachments' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'attachments' | i18n}}</span>
<ng-container *ngIf="showFixOldAttachments(c)">
<i class="fa fa-exclamation-triangle text-warning" appStopProp
title="{{'attachmentsNeedFix' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'attachmentsNeedFix' | i18n}}</span>
</ng-container>
</ng-container>
<br>
<small appStopProp>{{c.subTitle}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
appA11yTitle="{{'options' | i18n}}">
<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 && !c.isDeleted">
<a class="dropdown-item" href="#" appStopClick
(click)="copy(c, c.login.username, 'username', 'Username')">
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{'copyUsername' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick
(click)="copy(c, c.login.password, 'password', 'Password')" *ngIf="c.viewPassword">
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{'copyPassword' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="copy(c, c.login.totp, 'verificationCodeTotp', 'TOTP')"
*ngIf="displayTotpCopyButton(c)">
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{'copyVerificationCode' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick *ngIf="c.login.canLaunch"
(click)="launch(c.login.launchUri)">
<i class="fa fa-fw fa-share-square-o" aria-hidden="true"></i>
{{'launch' | i18n}}
</a>
</ng-container>
<a class="dropdown-item" href="#" appStopClick (click)="attachments(c)">
<i class="fa fa-fw fa-paperclip" aria-hidden="true"></i>
{{'attachments' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
(click)="clone(c)">
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
{{'clone' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick
*ngIf="!organization && !c.organizationId && !c.isDeleted" (click)="share(c)">
<i class="fa fa-fw fa-arrow-circle-o-right" aria-hidden="true"></i>
{{'moveToOrganization' | i18n}}
</a>
<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}}
</a>
<a class="dropdown-item" href="#" appStopClick *ngIf="c.organizationId && accessEvents"
(click)="events(c)">
<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>
{{(c.isDeleted ? 'permanentlyDelete' : 'delete') | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="!filteredCiphers.length">
<ng-container *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p>{{'noItemsInList' | i18n}}</p>
<button (click)="addCipher()" class="btn btn-outline-primary" *ngIf="showAddNew">
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}</button>
</ng-container>
</div>
<ng-container *ngIf="isPaging() ? pagedCiphers : ciphers as filteredCiphers">
<table
class="table table-hover table-list table-ciphers"
*ngIf="filteredCiphers.length"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let c of filteredCiphers">
<td (click)="checkCipher(c)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" appStopProp />
</td>
<td (click)="checkCipher(c)" class="table-list-icon">
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<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">
<i class="fa fa-cube" appStopProp title="{{ 'shared' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="fa fa-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
<ng-container *ngIf="showFixOldAttachments(c)">
<i
class="fa fa-exclamation-triangle text-warning"
appStopProp
title="{{ 'attachmentsNeedFix' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachmentsNeedFix" | i18n }}</span>
</ng-container>
</ng-container>
<br />
<small appStopProp>{{ c.subTitle }}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<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 && !c.isDeleted">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="copy(c, c.login.username, 'username', 'Username')"
>
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{ "copyUsername" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="copy(c, c.login.password, 'password', 'Password')"
*ngIf="c.viewPassword"
>
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{ "copyPassword" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="copy(c, c.login.totp, 'verificationCodeTotp', 'TOTP')"
*ngIf="displayTotpCopyButton(c)"
>
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{ "copyVerificationCode" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
*ngIf="c.login.canLaunch"
(click)="launch(c.login.launchUri)"
>
<i class="fa fa-fw fa-share-square-o" aria-hidden="true"></i>
{{ "launch" | i18n }}
</a>
</ng-container>
<a class="dropdown-item" href="#" appStopClick (click)="attachments(c)">
<i class="fa fa-fw fa-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
(click)="clone(c)"
>
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
{{ "clone" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
*ngIf="!organization && !c.organizationId && !c.isDeleted"
(click)="share(c)"
>
<i class="fa fa-fw fa-arrow-circle-o-right" aria-hidden="true"></i>
{{ "moveToOrganization" | i18n }}
</a>
<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 }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
*ngIf="c.organizationId && accessEvents"
(click)="events(c)"
>
<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>
{{ (c.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="!filteredCiphers.length">
<ng-container *ngIf="!loaded">
<i
class="fa fa-spinner fa-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p>{{ "noItemsInList" | i18n }}</p>
<button (click)="addCipher()" class="btn btn-outline-primary" *ngIf="showAddNew">
<i class="fa fa-plus fa-fw"></i>{{ "addItem" | i18n }}
</button>
</ng-container>
</div>
</ng-container>

View File

@@ -1,264 +1,292 @@
import {
Component,
EventEmitter,
Input,
OnDestroy,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { TotpService } from 'jslib-common/abstractions/totp.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { CiphersComponent as BaseCiphersComponent } from 'jslib-angular/components/ciphers.component';
import { CiphersComponent as BaseCiphersComponent } from "jslib-angular/components/ciphers.component";
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType';
import { CipherType } from 'jslib-common/enums/cipherType';
import { EventType } from 'jslib-common/enums/eventType';
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from "jslib-common/enums/eventType";
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CipherView } from "jslib-common/models/view/cipherView";
const MaxCheckedCount = 500;
@Component({
selector: 'app-vault-ciphers',
templateUrl: 'ciphers.component.html',
selector: "app-vault-ciphers",
templateUrl: "ciphers.component.html",
})
export class CiphersComponent extends BaseCiphersComponent implements OnDestroy {
@Input() showAddNew = true;
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
@Output() onShareClicked = new EventEmitter<CipherView>();
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
@Output() onCloneClicked = new EventEmitter<CipherView>();
@Input() showAddNew = true;
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
@Output() onShareClicked = new EventEmitter<CipherView>();
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
@Output() onCloneClicked = new EventEmitter<CipherView>();
pagedCiphers: CipherView[] = [];
pageSize = 200;
cipherType = CipherType;
actionPromise: Promise<any>;
userHasPremiumAccess = false;
pagedCiphers: CipherView[] = [];
pageSize = 200;
cipherType = CipherType;
actionPromise: Promise<any>;
userHasPremiumAccess = false;
private didScroll = false;
private pagedCiphersCount = 0;
private refreshing = false;
private didScroll = false;
private pagedCiphersCount = 0;
private refreshing = false;
constructor(searchService: SearchService,
protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService,
protected cipherService: CipherService, protected eventService: EventService,
protected totpService: TotpService, protected stateService: StateService,
protected passwordRepromptService: PasswordRepromptService, private logService: LogService) {
super(searchService);
constructor(
searchService: SearchService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected cipherService: CipherService,
protected eventService: EventService,
protected totpService: TotpService,
protected stateService: StateService,
protected passwordRepromptService: PasswordRepromptService,
private logService: LogService
) {
super(searchService);
}
async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
}
ngOnDestroy() {
this.selectAll(false);
}
loadMore() {
if (this.ciphers.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCiphers.length;
let pagedSize = this.pageSize;
if (this.refreshing && pagedLength === 0 && this.pagedCiphersCount > this.pageSize) {
pagedSize = this.pagedCiphersCount;
}
if (this.ciphers.length > pagedLength) {
this.pagedCiphers = this.pagedCiphers.concat(
this.ciphers.slice(pagedLength, pagedLength + pagedSize)
);
}
this.pagedCiphersCount = this.pagedCiphers.length;
this.didScroll = this.pagedCiphers.length > this.pageSize;
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter, this.deleted);
} finally {
this.refreshing = false;
}
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.ciphers.length > this.pageSize;
}
async resetPaging() {
this.pagedCiphers = [];
this.loadMore();
}
async doSearch(indexedCiphers?: CipherView[]) {
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
[this.filter, this.deletedFilter],
indexedCiphers
);
this.resetPaging();
}
launch(uri: string) {
this.platformUtilsService.launchUri(uri);
}
async attachments(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onAttachmentsClicked.emit(c);
}
async share(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onShareClicked.emit(c);
}
collections(c: CipherView) {
this.onCollectionsClicked.emit(c);
}
async clone(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onCloneClicked.emit(c);
}
async delete(c: CipherView): Promise<boolean> {
if (!(await this.repromptCipher(c))) {
return;
}
if (this.actionPromise != null) {
return;
}
const permanent = c.isDeleted;
const confirmed = await this.platformUtilsService.showDialog(
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;
}
async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
try {
this.actionPromise = this.deleteCipher(c.id, permanent);
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem")
);
this.refresh();
} catch (e) {
this.logService.error(e);
}
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;
}
ngOnDestroy() {
this.selectAll(false);
try {
this.actionPromise = this.cipherService.restoreWithServer(c.id);
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh();
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.repromptCipher(cipher))
) {
return;
}
loadMore() {
if (this.ciphers.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCiphers.length;
let pagedSize = this.pageSize;
if (this.refreshing && pagedLength === 0 && this.pagedCiphersCount > this.pageSize) {
pagedSize = this.pagedCiphersCount;
}
if (this.ciphers.length > pagedLength) {
this.pagedCiphers = this.pagedCiphers.concat(this.ciphers.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedCiphersCount = this.pagedCiphers.length;
this.didScroll = this.pagedCiphers.length > this.pageSize;
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter, this.deleted);
} finally {
this.refreshing = false;
}
if (!cipher.viewPassword) {
return;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.ciphers.length > this.pageSize;
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id);
} else if (typeI18nKey === "securityCode") {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
}
async resetPaging() {
this.pagedCiphers = [];
this.loadMore();
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
async doSearch(indexedCiphers?: CipherView[]) {
this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, this.deletedFilter], indexedCiphers);
this.resetPaging();
const selectCount =
select && this.ciphers.length > MaxCheckedCount ? MaxCheckedCount : this.ciphers.length;
for (let i = 0; i < selectCount; i++) {
this.checkCipher(this.ciphers[i], select);
}
}
launch(uri: string) {
this.platformUtilsService.launchUri(uri);
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);
}
async attachments(c: CipherView) {
if (!await this.repromptCipher(c)) {
return;
}
this.onAttachmentsClicked.emit(c);
getSelectedIds(): string[] {
return this.getSelected().map((c) => c.id);
}
displayTotpCopyButton(cipher: CipherView) {
return (
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
);
}
async selectCipher(cipher: CipherView) {
if (await this.repromptCipher(cipher)) {
super.selectCipher(cipher);
}
}
async share(c: CipherView) {
if (!await this.repromptCipher(c)) {
return;
}
this.onShareClicked.emit(c);
}
protected deleteCipher(id: string, permanent: boolean) {
return permanent
? this.cipherService.deleteWithServer(id)
: this.cipherService.softDeleteWithServer(id);
}
collections(c: CipherView) {
this.onCollectionsClicked.emit(c);
}
protected showFixOldAttachments(c: CipherView) {
return c.hasOldAttachments && c.organizationId == null;
}
async clone(c: CipherView) {
if (!await this.repromptCipher(c)) {
return;
}
this.onCloneClicked.emit(c);
}
async delete(c: CipherView): Promise<boolean> {
if (!await this.repromptCipher(c)) {
return;
}
if (this.actionPromise != null) {
return;
}
const permanent = c.isDeleted;
const confirmed = await this.platformUtilsService.showDialog(
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, permanent);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t(permanent ? 'permanentlyDeletedItem'
: 'deletedItem'));
this.refresh();
} catch (e) {
this.logService.error(e);
}
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.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem'));
this.refresh();
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (this.passwordRepromptService.protectedFields().includes(aType) && !await this.repromptCipher(cipher)) {
return;
}
if (value == null || aType === 'TOTP' && !this.displayTotpCopyButton(cipher)) {
return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
}
if (!cipher.viewPassword) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
if (typeI18nKey === 'password' || typeI18nKey === 'verificationCodeTotp') {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, cipher.id);
} else if (typeI18nKey === 'securityCode') {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
}
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);
}
displayTotpCopyButton(cipher: CipherView) {
return (cipher?.login?.hasTotp ?? false) &&
(cipher.organizationUseTotp || this.userHasPremiumAccess);
}
async selectCipher(cipher: CipherView) {
if (await this.repromptCipher(cipher)) {
super.selectCipher(cipher);
}
}
protected deleteCipher(id: string, permanent: boolean) {
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
}
protected showFixOldAttachments(c: CipherView) {
return c.hasOldAttachments && c.organizationId == null;
}
protected async repromptCipher(c: CipherView) {
return c.reprompt === CipherRepromptType.None || await this.passwordRepromptService.showPasswordPrompt();
}
protected async repromptCipher(c: CipherView) {
return (
c.reprompt === CipherRepromptType.None ||
(await this.passwordRepromptService.showPasswordPrompt())
);
}
}

View File

@@ -1,53 +1,63 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="collectionsTitle">
{{'collections' | i18n}}
<small *ngIf="cipher">{{cipher.name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'collectionsDesc' | i18n}}</p>
<div class="d-flex">
<h3>{{'collections' | i18n}}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{'noCollectionsInList' | i18n}}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
appStopProp>
</td>
<td>
{{c.name}}
</td>
</tr>
</tbody>
</table>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="collectionsTitle">
{{ "collections" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "collectionsDesc" | i18n }}</p>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,37 +1,39 @@
import {
Component,
OnDestroy,
} from '@angular/core';
import { Component, OnDestroy } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { CollectionView } from "jslib-common/models/view/collectionView";
import { CollectionsComponent as BaseCollectionsComponent } from 'jslib-angular/components/collections.component';
import { CollectionsComponent as BaseCollectionsComponent } from "jslib-angular/components/collections.component";
@Component({
selector: 'app-vault-collections',
templateUrl: 'collections.component.html',
selector: "app-vault-collections",
templateUrl: "collections.component.html",
})
export class CollectionsComponent extends BaseCollectionsComponent implements OnDestroy {
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, cipherService: CipherService, logService: LogService) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
}
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
logService: LogService
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
}
ngOnDestroy() {
this.selectAll(false);
}
ngOnDestroy() {
this.selectAll(false);
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
this.collections.forEach(c => this.check(c, select));
}
selectAll(select: boolean) {
this.collections.forEach((c) => this.check(c, select));
}
}

View File

@@ -1,34 +1,68 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="folderAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="folderAddEditTitle">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="folder.name" required
appAutofocus>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [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"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="folderAddEditTitle">{{ title }}</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="folder.name"
required
appAutofocus
/>
</div>
<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>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[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"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,21 +1,23 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import {
FolderAddEditComponent as BaseFolderAddEditComponent,
} from 'jslib-angular/components/folder-add-edit.component';
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "jslib-angular/components/folder-add-edit.component";
@Component({
selector: 'app-folder-add-edit',
templateUrl: 'folder-add-edit.component.html',
selector: "app-folder-add-edit",
templateUrl: "folder-add-edit.component.html",
})
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
constructor(folderService: FolderService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, logService: LogService) {
super(folderService, i18nService, platformUtilsService, logService);
}
constructor(
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(folderService, i18nService, platformUtilsService, logService);
}
}

View File

@@ -1,116 +1,169 @@
<div class="card vault-filters">
<div class="card-header d-flex">
{{'filters' | i18n}}
<a class="ml-auto" href="https://help.bitwarden.com/article/searching-vault/" target="_blank" rel="noopener"
appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
<div class="card-header d-flex">
{{ "filters" | i18n }}
<a
class="ml-auto"
href="https://help.bitwarden.com/article/searching-vault/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<input
type="search"
placeholder="{{ searchPlaceholder || ('searchVault' | i18n) }}"
id="search"
class="form-control"
[(ngModel)]="searchText"
(input)="searchTextChanged()"
autocomplete="off"
appAutofocus
/>
<ul class="fa-ul card-ul">
<li [ngClass]="{ active: selectedAll }">
<a href="#" appStopClick (click)="selectAll()">
<i class="fa-li fa fa-fw fa-th"></i>{{ "allItems" | i18n }}
</a>
</div>
<div class="card-body">
<input type="search" placeholder="{{searchPlaceholder || ('searchVault' | i18n)}}" id="search"
class="form-control" [(ngModel)]="searchText" (input)="searchTextChanged()" autocomplete="off" appAutofocus>
</li>
<li [ngClass]="{ active: selectedFavorites }" *ngIf="showFavorites">
<a href="#" appStopClick (click)="selectFavorites()">
<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">
<li [ngClass]="{ active: selectedType === cipherType.Login }">
<a href="#" appStopClick (click)="selectType(cipherType.Login)">
<i class="fa-li fa fa-fw fa-globe"></i>{{ "typeLogin" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Card }">
<a href="#" appStopClick (click)="selectType(cipherType.Card)">
<i class="fa-li fa fa-fw fa-credit-card"></i>{{ "typeCard" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Identity }">
<a href="#" appStopClick (click)="selectType(cipherType.Identity)">
<i class="fa-li fa fa-fw fa-id-card-o"></i>{{ "typeIdentity" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.SecureNote }">
<a href="#" appStopClick (click)="selectType(cipherType.SecureNote)">
<i class="fa-li fa fa-fw fa-sticky-note-o"></i>{{ "typeSecureNote" | i18n }}
</a>
</li>
</ul>
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
<ng-container *ngIf="loaded">
<ng-container *ngIf="showFolders">
<h3 class="d-flex">
{{ "folders" | i18n }}
<a
href="#"
class="text-muted ml-auto"
appStopClick
(click)="addFolder()"
appA11yTitle="{{ 'addFolder' | i18n }}"
>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
</a>
</h3>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedAll}">
<a href="#" appStopClick (click)="selectAll()">
<i class="fa-li fa fa-fw fa-th"></i>{{'allItems' | i18n}}
<ng-template #recursiveFolders let-folders>
<li
*ngFor="let f of folders"
[ngClass]="{ active: selectedFolder && f.node.id === selectedFolderId }"
>
<div class="d-flex">
<i
*ngIf="f.children.length"
class="fa-li fa"
title="{{ 'toggleCollapse' | i18n }}"
[ngClass]="{
'fa-caret-right': isCollapsed(f.node),
'fa-caret-down': !isCollapsed(f.node)
}"
(click)="collapse(f.node)"
></i>
<a href="#" class="text-break" appStopClick (click)="selectFolder(f.node)">
<i
*ngIf="f.children.length === 0"
class="fa-li fa fa-folder-o"
aria-hidden="true"
></i
>{{ f.node.name }}
</a>
</li>
<li [ngClass]="{active: selectedFavorites}" *ngIf="showFavorites">
<a href="#" appStopClick (click)="selectFavorites()">
<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
href="#"
class="text-muted ml-auto show-active"
appStopClick
(click)="editFolder(f.node)"
appA11yTitle="{{ 'editFolder' | i18n }}"
*ngIf="f.node.id"
>
<i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
</a>
</div>
<ul class="fa-ul card-ul carets" *ngIf="f.children.length && !isCollapsed(f.node)">
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container *ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }">
</ng-container>
</ul>
<h3>{{'types' | i18n}}</h3>
</ng-container>
<ng-container *ngIf="showCollections && collections && collections.length">
<h3>{{ "collections" | i18n }}</h3>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedType === cipherType.Login}">
<a href="#" appStopClick (click)="selectType(cipherType.Login)">
<i class="fa-li fa fa-fw fa-globe"></i>{{'typeLogin' | i18n}}
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.Card}">
<a href="#" appStopClick (click)="selectType(cipherType.Card)">
<i class="fa-li fa fa-fw fa-credit-card"></i>{{'typeCard' | i18n}}
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.Identity}">
<a href="#" appStopClick (click)="selectType(cipherType.Identity)">
<i class="fa-li fa fa-fw fa-id-card-o"></i>{{'typeIdentity' | i18n}}
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.SecureNote}">
<a href="#" appStopClick (click)="selectType(cipherType.SecureNote)">
<i class="fa-li fa fa-fw fa-sticky-note-o"></i>{{'typeSecureNote' | i18n}}
</a>
<ng-template #recursiveCollections let-collections>
<li
*ngFor="let c of collections"
[ngClass]="{ active: c.node.id === selectedCollectionId }"
>
<i
*ngIf="c.children.length"
class="fa-li fa"
title="{{ 'toggleCollapse' | i18n }}"
[ngClass]="{
'fa-caret-right': isCollapsed(c.node),
'fa-caret-down': !isCollapsed(c.node)
}"
(click)="collapse(c.node)"
></i>
<a href="#" class="text-break" appStopClick (click)="selectCollection(c.node)">
<i *ngIf="c.children.length === 0" class="fa-li fa fa-cube" aria-hidden="true"></i
>{{ c.node.name }}
</a>
<ul class="fa-ul card-ul carets" *ngIf="c.children.length && !isCollapsed(c.node)">
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
>
</ng-container>
</ul>
<p *ngIf="!loaded" class="text-muted">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
<ng-container *ngIf="loaded">
<ng-container *ngIf="showFolders">
<h3 class="d-flex">
{{'folders' | i18n}}
<a href="#" class="text-muted ml-auto" appStopClick (click)="addFolder()"
appA11yTitle="{{'addFolder' | i18n}}">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
</a>
</h3>
<ul class="fa-ul card-ul">
<ng-template #recursiveFolders let-folders>
<li *ngFor="let f of folders"
[ngClass]="{active: selectedFolder && f.node.id === selectedFolderId}">
<div class="d-flex">
<i *ngIf="f.children.length" class="fa-li fa" title="{{'toggleCollapse' | i18n}}"
[ngClass]="{'fa-caret-right': isCollapsed(f.node), 'fa-caret-down': !isCollapsed(f.node)}"
(click)="collapse(f.node)"></i>
<a href="#" class="text-break" appStopClick (click)="selectFolder(f.node)">
<i *ngIf="f.children.length === 0" class="fa-li fa fa-folder-o" aria-hidden="true"></i>{{f.node.name}}
</a>
<a href="#" class="text-muted ml-auto show-active" appStopClick
(click)="editFolder(f.node)" appA11yTitle="{{'editFolder' | i18n}}"
*ngIf="f.node.id">
<i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
</a>
</div>
<ul class="fa-ul card-ul carets" *ngIf="f.children.length && !isCollapsed(f.node)">
<ng-container *ngTemplateOutlet="recursiveFolders; context:{ $implicit: f.children }">
</ng-container>
</ul>
</li>
</ng-template>
<ng-container *ngTemplateOutlet="recursiveFolders; context:{ $implicit: nestedFolders }">
</ng-container>
</ul>
</ng-container>
<ng-container *ngIf="showCollections && collections && collections.length">
<h3>{{'collections' | i18n}}</h3>
<ul class="fa-ul card-ul">
<ng-template #recursiveCollections let-collections>
<li *ngFor="let c of collections" [ngClass]="{active: c.node.id === selectedCollectionId}">
<i *ngIf="c.children.length" class="fa-li fa" title="{{'toggleCollapse' | i18n}}"
[ngClass]="{'fa-caret-right': isCollapsed(c.node), 'fa-caret-down': !isCollapsed(c.node)}"
(click)="collapse(c.node)"></i>
<a href="#" class="text-break" appStopClick (click)="selectCollection(c.node)">
<i *ngIf="c.children.length === 0" class="fa-li fa fa-cube" aria-hidden="true"></i>{{c.node.name}}
</a>
<ul class="fa-ul card-ul carets" *ngIf="c.children.length && !isCollapsed(c.node)">
<ng-container
*ngTemplateOutlet="recursiveCollections; context:{ $implicit: c.children }">
</ng-container>
</ul>
</li>
</ng-template>
<ng-container *ngTemplateOutlet="recursiveCollections; context:{ $implicit: nestedCollections }">
</ng-container>
</ul>
</ng-container>
</ng-container>
</div>
</ng-container>
</ng-container>
</div>
</div>

View File

@@ -1,31 +1,30 @@
import {
Component,
EventEmitter,
Output,
} from '@angular/core';
import { Component, EventEmitter, Output } from "@angular/core";
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { GroupingsComponent as BaseGroupingsComponent } from 'jslib-angular/components/groupings.component';
import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component";
@Component({
selector: 'app-vault-groupings',
templateUrl: 'groupings.component.html',
selector: "app-vault-groupings",
templateUrl: "groupings.component.html",
})
export class GroupingsComponent extends BaseGroupingsComponent {
@Output() onSearchTextChanged = new EventEmitter<string>();
@Output() onSearchTextChanged = new EventEmitter<string>();
searchText: string = '';
searchPlaceholder: string = null;
searchText: string = "";
searchPlaceholder: string = null;
constructor(collectionService: CollectionService, folderService: FolderService,
stateService: StateService) {
super(collectionService, folderService, stateService);
}
constructor(
collectionService: CollectionService,
folderService: FolderService,
stateService: StateService
) {
super(collectionService, folderService, stateService);
}
searchTextChanged() {
this.onSearchTextChanged.emit(this.searchText);
}
searchTextChanged() {
this.onSearchTextChanged.emit(this.searchText);
}
}

View File

@@ -1,68 +1,92 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="shareTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="shareTitle">
{{'moveToOrganization' | i18n}}
<small *ngIf="cipher">{{cipher.name}}</small>
</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="!organizations || !organizations.length">
{{'noOrganizationsList' | i18n}}
</div>
<div class="modal-body" *ngIf="organizations && organizations.length">
<p>{{'moveToOrgDesc' | i18n}}</p>
<div class="form-group">
<label for="organization">{{'organization' | i18n}}</label>
<select id="organization" name="OrganizationId" [(ngModel)]="organizationId" class="form-control"
(change)="filterCollections()">
<option *ngFor="let o of organizations" [ngValue]="o.id">{{o.name}}</option>
</select>
</div>
<div class="d-flex">
<h3>{{'collections' | i18n}}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{'noCollectionsInList' | i18n}}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
appStopProp>
</td>
<td>
{{c.name}}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit manual" [disabled]="form.loading || !canSave"
[ngClass]="{loading:form.loading}" *ngIf="organizations && organizations.length">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<a href="#" routerLink="/settings/create-organization" class="btn btn-primary"
*ngIf="!organizations || !organizations.length">
{{'newOrganization' | i18n}}
</a>
<button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button>
</div>
</form>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="shareTitle">
{{ "moveToOrganization" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="!organizations || !organizations.length">
{{ "noOrganizationsList" | i18n }}
</div>
<div class="modal-body" *ngIf="organizations && organizations.length">
<p>{{ "moveToOrgDesc" | i18n }}</p>
<div class="form-group">
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
class="form-control"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[disabled]="form.loading || !canSave"
[ngClass]="{ loading: form.loading }"
*ngIf="organizations && organizations.length"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<a
href="#"
routerLink="/settings/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>
{{ "newOrganization" | i18n }}
</a>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,41 +1,49 @@
import {
Component,
OnDestroy,
} from '@angular/core';
import { Component, OnDestroy } from "@angular/core";
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { CollectionView } from "jslib-common/models/view/collectionView";
import { ShareComponent as BaseShareComponent } from 'jslib-angular/components/share.component';
import { LogService } from 'jslib-common/abstractions/log.service';
import { ShareComponent as BaseShareComponent } from "jslib-angular/components/share.component";
import { LogService } from "jslib-common/abstractions/log.service";
@Component({
selector: 'app-vault-share',
templateUrl: 'share.component.html',
selector: "app-vault-share",
templateUrl: "share.component.html",
})
export class ShareComponent extends BaseShareComponent implements OnDestroy {
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, cipherService: CipherService,
organizationService: OrganizationService, logService: LogService) {
super(collectionService, platformUtilsService, i18nService, cipherService,
logService, organizationService);
}
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
logService: LogService
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService
);
}
ngOnDestroy() {
this.selectAll(false);
}
ngOnDestroy() {
this.selectAll(false);
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach(c => this.check(c, select));
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach((c) => this.check(c, select));
}
}

View File

@@ -1,115 +1,147 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<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)"
(onTrashClicked)="filterDeleted()">
</app-vault-groupings>
</div>
<div class="col-6">
<div class="page-header d-flex">
<h1>
{{'myVault' | i18n}}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
</app-vault-bulk-actions>
<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>
</div>
<app-callout type="warning" *ngIf="deleted" icon="fa-warning">
{{trashCleanupWarning}}
</app-callout>
<app-vault-ciphers (onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
(onShareClicked)="shareCipher($event)" (onCollectionsClicked)="editCipherCollections($event)"
(onCloneClicked)="cloneCipher($event)">
</app-vault-ciphers>
</div>
<div class="col-3">
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{'updateKeyTitle' | i18n}}
</div>
<div class="card-body">
<p>{{'updateEncryptionKeyShortDesc' | i18n}}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{'updateEncryptionKey' | i18n}}
</button>
</div>
</div>
<app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email>
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{'updateBrowser' | i18n}}
</div>
<div class="card-body">
<p>{{'updateBrowserDesc' | i18n}}</p>
<a class="btn btn-block btn-outline-secondary" target="_blank"
href="https://browser-update.org/update-browser.html" rel="noopener">
{{'updateBrowser' | i18n}}
</a>
</div>
</div>
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
<div class="card-header bg-success text-white">
<i class="fa fa-star fa-fw" aria-hidden="true"></i> {{'goPremium' | i18n}}
</div>
<div class="card-body">
<p>{{'premiumUpgradeUnlockFeatures' | i18n}}</p>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
{{'goPremium' | i18n}}
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
{{'organizations' | i18n}}
<a class="ml-auto" href="https://help.bitwarden.com/article/what-is-an-organization/"
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<app-organizations [vault]="true"></app-organizations>
</div>
</div>
<div class="card border-success mb-4" *ngIf="showRedeemSponsorship">
<div class="card-header bg-success text-white">
{{'freeFamiliesPlan' | i18n}}
</div>
<div class="card-body">
<p>{{'sponsoredFamiliesEligible' | i18n}}</p>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/sponsored-families">
{{'redeemNow' | i18n}}
</a>
</div>
</div>
<div class="card mt-4" *ngIf="showProviders">
<div class="card-header d-flex">
{{'providers' | i18n}}
<a class="ml-auto" href="https://bitwarden.com/help/article/about-providers/"
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<app-providers vault="true"></app-providers>
</div>
</div>
</div>
<div class="row">
<div class="col-3">
<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)"
(onTrashClicked)="filterDeleted()"
>
</app-vault-groupings>
</div>
<div class="col-6">
<div class="page-header d-flex">
<h1>
{{ "myVault" | i18n }}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i
class="fa fa-spinner fa-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
</app-vault-bulk-actions>
<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>
</div>
<app-callout type="warning" *ngIf="deleted" icon="fa-warning">
{{ trashCleanupWarning }}
</app-callout>
<app-vault-ciphers
(onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)"
(onAddCipher)="addCipher()"
(onShareClicked)="shareCipher($event)"
(onCollectionsClicked)="editCipherCollections($event)"
(onCloneClicked)="cloneCipher($event)"
>
</app-vault-ciphers>
</div>
<div class="col-3">
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{ "updateKeyTitle" | i18n }}
</div>
<div class="card-body">
<p>{{ "updateEncryptionKeyShortDesc" | i18n }}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{ "updateEncryptionKey" | i18n }}
</button>
</div>
</div>
<app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email>
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{ "updateBrowser" | i18n }}
</div>
<div class="card-body">
<p>{{ "updateBrowserDesc" | i18n }}</p>
<a
class="btn btn-block btn-outline-secondary"
target="_blank"
href="https://browser-update.org/update-browser.html"
rel="noopener"
>
{{ "updateBrowser" | i18n }}
</a>
</div>
</div>
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
<div class="card-header bg-success text-white">
<i class="fa fa-star fa-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
</div>
<div class="card-body">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
{{ "goPremium" | i18n }}
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
{{ "organizations" | i18n }}
<a
class="ml-auto"
href="https://help.bitwarden.com/article/what-is-an-organization/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<app-organizations [vault]="true"></app-organizations>
</div>
</div>
<div class="card border-success mb-4" *ngIf="showRedeemSponsorship">
<div class="card-header bg-success text-white">
{{ "freeFamiliesPlan" | i18n }}
</div>
<div class="card-body">
<p>{{ "sponsoredFamiliesEligible" | i18n }}</p>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/sponsored-families">
{{ "redeemNow" | i18n }}
</a>
</div>
</div>
<div class="card mt-4" *ngIf="showProviders">
<div class="card-header d-flex">
{{ "providers" | i18n }}
<a
class="ml-auto"
href="https://bitwarden.com/help/article/about-providers/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<app-providers vault="true"></app-providers>
</div>
</div>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #folderAddEdit></ng-template>

View File

@@ -1,362 +1,403 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from 'rxjs/operators';
import { first } from "rxjs/operators";
import { CipherType } from 'jslib-common/enums/cipherType';
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CipherView } from "jslib-common/models/view/cipherView";
import { OrganizationsComponent } from '../settings/organizations.component';
import { UpdateKeyComponent } from '../settings/update-key.component';
import { AddEditComponent } from './add-edit.component';
import { AttachmentsComponent } from './attachments.component';
import { CiphersComponent } from './ciphers.component';
import { CollectionsComponent } from './collections.component';
import { FolderAddEditComponent } from './folder-add-edit.component';
import { GroupingsComponent } from './groupings.component';
import { ShareComponent } from './share.component';
import { OrganizationsComponent } from "../settings/organizations.component";
import { UpdateKeyComponent } from "../settings/update-key.component";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
import { CiphersComponent } from "./ciphers.component";
import { CollectionsComponent } from "./collections.component";
import { FolderAddEditComponent } from "./folder-add-edit.component";
import { GroupingsComponent } from "./groupings.component";
import { ShareComponent } from "./share.component";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { OrganizationService } from 'jslib-common/abstractions/organization.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ProviderService } from 'jslib-common/abstractions/provider.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
const BroadcasterSubscriptionId = 'VaultComponent';
const BroadcasterSubscriptionId = "VaultComponent";
@Component({
selector: 'app-vault',
templateUrl: 'vault.component.html',
selector: "app-vault",
templateUrl: "vault.component.html",
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild(OrganizationsComponent, { static: true }) organizationsComponent: OrganizationsComponent;
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
@ViewChild('folderAddEdit', { read: ViewContainerRef, static: true }) folderAddEditModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('share', { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
@ViewChild('updateKeyTemplate', { read: ViewContainerRef, static: true }) updateKeyModalRef: ViewContainerRef;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild(OrganizationsComponent, { static: true })
organizationsComponent: OrganizationsComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild("collections", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
updateKeyModalRef: ViewContainerRef;
favorites: boolean = false;
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
showVerifyEmail = false;
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
showRedeemSponsorship = false;
showProviders = false;
deleted: boolean = false;
trashCleanupWarning: string = null;
favorites: boolean = false;
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
showVerifyEmail = false;
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
showRedeemSponsorship = false;
showProviders = false;
deleted: boolean = false;
trashCleanupWarning: string = null;
constructor(
private syncService: SyncService,
private route: ActivatedRoute,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService,
private modalService: ModalService,
private tokenService: TokenService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private stateService: StateService,
private organizationService: OrganizationService,
private providerService: ProviderService
) {}
constructor(private syncService: SyncService, private route: ActivatedRoute,
private router: Router, private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService, private modalService: ModalService,
private tokenService: TokenService, private cryptoService: CryptoService,
private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService,
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private stateService: StateService, private organizationService: OrganizationService,
private providerService: ProviderService) { }
async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
: "trashCleanupWarning"
);
async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
this.showBrowserOutdated = window.navigator.userAgent.indexOf('MSIE') !== -1;
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost() ? 'trashCleanupWarningSelfHosted' : 'trashCleanupWarning'
);
this.route.queryParams.pipe(first()).subscribe(async (params) => {
await this.syncService.fullSync(false);
this.route.queryParams.pipe(first()).subscribe(async params => {
await this.syncService.fullSync(false);
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumCallout =
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumCallout = !this.showVerifyEmail && !canAccessPremium &&
!this.platformUtilsService.isSelfHost();
this.showProviders = (await this.providerService.getAll()).length > 0;
this.showProviders = (await this.providerService.getAll()).length > 0;
const allOrgs = await this.organizationService.getAll();
this.showRedeemSponsorship =
allOrgs.some((o) => o.familySponsorshipAvailable) &&
!allOrgs.some((o) => o.familySponsorshipFriendlyName != null);
const allOrgs = await this.organizationService.getAll();
this.showRedeemSponsorship = allOrgs.some(o => o.familySponsorshipAvailable) && !allOrgs.some(o => o.familySponsorshipFriendlyName != null);
await Promise.all([this.groupingsComponent.load(), this.organizationsComponent.load()]);
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
await Promise.all([
this.groupingsComponent.load(),
this.organizationsComponent.load(),
]);
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
} else {
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) {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
} else {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
}
}
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncCompleted':
if (message.successfully) {
await Promise.all([
this.groupingsComponent.load(),
this.organizationsComponent.load(),
this.ciphersComponent.load(this.ciphersComponent.filter),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
this.clearFilters();
this.go();
}
async filterFavorites() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchFavorites');
await this.ciphersComponent.reload(c => c.favorite);
this.clearFilters();
this.favorites = true;
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');
await this.ciphersComponent.reload(c => c.type === type);
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
this.ciphersComponent.showAddNew = true;
folderId = folderId === 'none' ? null : folderId;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchFolder');
await this.ciphersComponent.reload(c => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? 'none' : folderId;
this.go();
}
async filterCollection(collectionId: string) {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
await this.ciphersComponent.reload(c => c.collectionIds != null &&
c.collectionIds.indexOf(collectionId) > -1);
this.clearFilters();
this.collectionId = collectionId;
this.go();
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
async editCipherAttachments(cipher: CipherView) {
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (cipher.organizationId == null && !canAccessPremium) {
this.messagingService.send('premiumRequired');
return;
} else if (cipher.organizationId != null) {
const org = await this.organizationService.get(cipher.organizationId);
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
this.messagingService.send('upgradeOrganization', { organizationId: cipher.organizationId });
return;
}
} else {
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) {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
} else {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
}
}
let madeAttachmentChanges = false;
const [modal] = await this.modalService.openViewRef(AttachmentsComponent, this.attachmentsModalRef, comp => {
comp.cipherId = cipher.id;
comp.onUploadedAttachment.subscribe(() => madeAttachmentChanges = true);
comp.onDeletedAttachment.subscribe(() => madeAttachmentChanges = true);
comp.onReuploadedAttachment.subscribe(() => madeAttachmentChanges = true);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.groupingsComponent.load(),
this.organizationsComponent.load(),
this.ciphersComponent.load(this.ciphersComponent.filter),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
});
}
modal.onClosed.subscribe(async () => {
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async clearGroupingFilters() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchVault");
await this.ciphersComponent.reload();
this.clearFilters();
this.go();
}
async filterFavorites() {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchFavorites");
await this.ciphersComponent.reload((c) => c.favorite);
this.clearFilters();
this.favorites = true;
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");
await this.ciphersComponent.reload((c) => c.type === type);
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
this.ciphersComponent.showAddNew = true;
folderId = folderId === "none" ? null : folderId;
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchFolder");
await this.ciphersComponent.reload((c) => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? "none" : folderId;
this.go();
}
async filterCollection(collectionId: string) {
this.ciphersComponent.showAddNew = true;
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchCollection");
await this.ciphersComponent.reload(
(c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1
);
this.clearFilters();
this.collectionId = collectionId;
this.go();
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
async editCipherAttachments(cipher: CipherView) {
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (cipher.organizationId == null && !canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
} else if (cipher.organizationId != null) {
const org = await this.organizationService.get(cipher.organizationId);
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
this.messagingService.send("upgradeOrganization", {
organizationId: cipher.organizationId,
});
return;
}
}
async shareCipher(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(ShareComponent, this.shareModalRef, comp => {
comp.cipherId = cipher.id;
comp.onSharedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
let madeAttachmentChanges = false;
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onReuploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
}
);
modal.onClosed.subscribe(async () => {
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
});
}
async shareCipher(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
ShareComponent,
this.shareModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSharedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
}
);
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(CollectionsComponent, this.collectionsModalRef, comp => {
comp.cipherId = cipher.id;
comp.onSavedCollections.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSavedCollections.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
}
);
}
async addFolder() {
const [modal] = await this.modalService.openViewRef(FolderAddEditComponent, this.folderAddEditModalRef, comp => {
comp.folderId = null;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
});
async addFolder() {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = null;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
});
}
}
);
}
async editFolder(folderId: string) {
const [modal] = await this.modalService.openViewRef(FolderAddEditComponent, this.folderAddEditModalRef, comp => {
comp.folderId = folderId;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
});
comp.onDeletedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
await this.filterFolder('none');
this.groupingsComponent.selectedFolderId = null;
});
async editFolder(folderId: string) {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = folderId;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
});
}
async addCipher() {
const component = await this.editCipher(null);
component.type = this.type;
component.folderId = this.folderId === 'none' ? null : this.folderId;
if (this.collectionId != null) {
const collection = this.groupingsComponent.collections.filter(c => c.id === this.collectionId);
if (collection.length > 0) {
component.organizationId = collection[0].organizationId;
component.collectionIds = [this.collectionId];
}
}
}
async editCipher(cipher: CipherView) {
const [modal, childComponent] = await this.modalService.openViewRef(AddEditComponent, this.cipherAddEditModalRef, comp => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.onSavedCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onRestoredCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedFolder.subscribe(async () => {
modal.close();
await this.groupingsComponent.loadFolders();
await this.filterFolder("none");
this.groupingsComponent.selectedFolderId = null;
});
}
);
}
return childComponent;
async addCipher() {
const component = await this.editCipher(null);
component.type = this.type;
component.folderId = this.folderId === "none" ? null : this.folderId;
if (this.collectionId != null) {
const collection = this.groupingsComponent.collections.filter(
(c) => c.id === this.collectionId
);
if (collection.length > 0) {
component.organizationId = collection[0].organizationId;
component.collectionIds = [this.collectionId];
}
}
}
async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher);
component.cloneMode = true;
}
async updateKey() {
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
}
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
this.deleted = false;
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
favorites: this.favorites ? true : null,
type: this.type,
folderId: this.folderId,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
};
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
replaceUrl: true,
async editCipher(cipher: CipherView) {
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.onSavedCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onRestoredCipher.subscribe(async (c: CipherView) => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
return childComponent;
}
async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher);
component.cloneMode = true;
}
async updateKey() {
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
}
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
this.deleted = false;
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
favorites: this.favorites ? true : null,
type: this.type,
folderId: this.folderId,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
};
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
replaceUrl: true,
});
}
}