mirror of
https://github.com/bitwarden/browser
synced 2026-01-02 16:43:19 +00:00
[SG-998] and [SG-999] Vault and Autofill team refactor (#4542)
* Move DeprecatedVaultFilterService to vault folder * [libs] move VaultItemsComponent * [libs] move AddEditComponent * [libs] move AddEditCustomFields * [libs] move attachmentsComponent * [libs] folderAddEditComponent * [libs] IconComponent * [libs] PasswordRepormptComponent * [libs] PremiumComponent * [libs] ViewCustomFieldsComponent * [libs] ViewComponent * [libs] PasswordRepromptService * [libs] Move FolderService and FolderApiService abstractions * [libs] FolderService imports * [libs] PasswordHistoryComponent * [libs] move Sync and SyncNotifier abstractions * [libs] SyncService imports * [libs] fix file casing for passwordReprompt abstraction * [libs] SyncNotifier import fix * [libs] CipherServiceAbstraction * [libs] PasswordRepromptService abstraction * [libs] Fix file casing for angular passwordReprompt service * [libs] fix file casing for SyncNotifierService * [libs] CipherRepromptType * [libs] rename CipherRepromptType * [libs] CipherType * [libs] Rename CipherType * [libs] CipherData * [libs] FolderData * [libs] PasswordHistoryData * [libs] AttachmentData * [libs] CardData * [libs] FieldData * [libs] IdentityData * [libs] LocalData * [libs] LoginData * [libs] SecureNoteData * [libs] LoginUriData * [libs] Domain classes * [libs] SecureNote * [libs] Request models * [libs] Response models * [libs] View part 1 * [libs] Views part 2 * [libs] Move folder services * [libs] Views fixes * [libs] Move sync services * [libs] cipher service * [libs] Types * [libs] Sync file casing * [libs] Fix folder service import * [libs] Move spec files * [libs] casing fixes on spec files * [browser] Autofill background, clipboard, commands * [browser] Fix ContextMenusBackground casing * [browser] Rename fix * [browser] Autofill content * [browser] autofill.js * [libs] enpass importer spec fix * [browser] autofill models * [browser] autofill manifest path updates * [browser] Autofill notification files * [browser] autofill services * [browser] Fix file casing * [browser] Vault popup loose components * [browser] Vault components * [browser] Manifest fixes * [browser] Vault services * [cli] vault commands and models * [browser] File capitilization fixes * [desktop] Vault components and services * [web] vault loose components * [web] Vault components * [browser] Fix misc-utils import * [libs] Fix psono spec imports * [fix] Add comments to address lint rules
This commit is contained in:
@@ -1,176 +0,0 @@
|
||||
<ng-container>
|
||||
<h3 class="mt-4">{{ "customFields" | i18n }}</h3>
|
||||
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
|
||||
<div
|
||||
role="group"
|
||||
class="row"
|
||||
cdkDrag
|
||||
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
|
||||
attr.aria-label="{{ f.name }}"
|
||||
>
|
||||
<div class="col-5 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="fieldName{{ i }}">{{ "name" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/custom-fields/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" 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"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</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"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(f.value, 'value', 'Field')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-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)"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
/>
|
||||
<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="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-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="bwi bwi-lg bwi-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"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
>
|
||||
<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"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-danger ml-2"
|
||||
(click)="removeField(f)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-muted cursor-move"
|
||||
appA11yTitle="{{ 'dragToSort' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-hamburger bwi-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 && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "newCustomField" | i18n }}
|
||||
</a>
|
||||
<div class="row" *ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)">
|
||||
<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>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/components/add-edit-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
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;
|
||||
|
||||
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
|
||||
super(i18nService, eventCollectionService);
|
||||
}
|
||||
}
|
||||
@@ -1,979 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="cipherAddEditTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="cipherAddEditTitle">{{ title }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="cipher">
|
||||
<app-callout type="info" *ngIf="allowOwnershipAssignment() && !allowPersonal">
|
||||
{{ "personalOwnershipPolicyInEffect" | i18n }}
|
||||
</app-callout>
|
||||
<div class="row" *ngIf="!editMode && !viewOnly">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{ "whatTypeOfItem" | i18n }}</label>
|
||||
<select
|
||||
id="type"
|
||||
name="Type"
|
||||
[(ngModel)]="cipher.type"
|
||||
class="form-control"
|
||||
[disabled]="cipher.isDeleted"
|
||||
appAutofocus
|
||||
>
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="cipher.name"
|
||||
required
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group" *ngIf="!organization">
|
||||
<label for="folder">{{ "folder" | i18n }}</label>
|
||||
<select
|
||||
id="folder"
|
||||
name="FolderId"
|
||||
[(ngModel)]="cipher.folderId"
|
||||
class="form-control"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<ng-container *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="loginUsername">{{ "username" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="loginUsername"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div class="input-group-append" *ngIf="!cipher.isDeleted">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="loginPassword">{{ "password" | i18n }}</label>
|
||||
<div class="ml-auto d-flex" *ngIf="!cipher.isDeleted && !viewOnly">
|
||||
<a
|
||||
href="#"
|
||||
class="d-block mr-2 bwi-icon-above-input"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generatePassword' | i18n }}"
|
||||
(click)="generatePassword()"
|
||||
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-fw bwi-generate" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="d-block bwi-icon-above-input"
|
||||
#checkPasswordBtn
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-fw bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-fw bwi-spinner bwi-spin"
|
||||
aria-hidden="true"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
></i>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="d-block bwi-icon-above-input"
|
||||
appStopClick
|
||||
[appA11yTitle]="'toggleCharacterCount' | i18n"
|
||||
(click)="togglePasswordCount()"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-fw bwi-numbered-list" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="loginPassword"
|
||||
class="form-control text-monospace"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password"
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted || !cipher.viewPassword || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
[disabled]="!cipher.viewPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')"
|
||||
[disabled]="!cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showPasswordCount" class="tw-mb-4">
|
||||
<label>{{ "passwordCharacterCount" | i18n }}</label>
|
||||
<div class="tw-flex tw-justify-between">
|
||||
<bit-color-password
|
||||
[password]="cipher.login.password"
|
||||
[showCount]="true"
|
||||
></bit-color-password>
|
||||
<button type="button" bitLink (click)="togglePasswordCount()">
|
||||
{{ "hide" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-mb-4 tw-w-1/2">
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="form-control text-monospace"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || !cipher.viewPassword || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-mb-4 tw-ml-4 tw-flex tw-w-1/2 tw-items-end" [ngClass]="{ low: totpLow }">
|
||||
<div
|
||||
class="totp tw-flex tw-flex-row tw-items-center"
|
||||
*ngIf="!cipher.login.totp || !totpCode"
|
||||
>
|
||||
<span class="totp-countdown">
|
||||
<span class="totp-sec tw-text-muted">15</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle
|
||||
class="totp-circle-muted inner"
|
||||
r="12.6"
|
||||
cy="16"
|
||||
cx="16"
|
||||
opacity="0.25"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': 40 }"
|
||||
></circle>
|
||||
<circle
|
||||
class="totp-circle-muted outer"
|
||||
opacity="0.25"
|
||||
r="14"
|
||||
cy="16"
|
||||
cx="16"
|
||||
></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="totp-code tw-mr-3 tw-ml-2 tw-text-muted"
|
||||
title="{{ 'verificationCodeTotp' | i18n }}"
|
||||
>--- ---</span
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone tw-text-muted" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="tw-pb-2" *ngIf="!cipher.login.totp || !totpCode">
|
||||
<app-premium-badge
|
||||
*ngIf="!organization && !cipher.organizationId"
|
||||
class="ml-3"
|
||||
></app-premium-badge>
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
bitBadge
|
||||
badgeType="primary"
|
||||
class="tw-ml-4"
|
||||
(click)="upgradeOrganization()"
|
||||
*ngIf="
|
||||
(organization && !organization.useTotp) ||
|
||||
(!organization &&
|
||||
!canAccessPremium &&
|
||||
cipher.organizationId &&
|
||||
!cipher.organizationUseTotp)
|
||||
"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="cipher.login.totp && totpCode"
|
||||
class="totp tw-flex tw-flex-row tw-items-center"
|
||||
>
|
||||
<span class="totp-countdown">
|
||||
<span class="totp-sec">{{ totpSec }}</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle
|
||||
class="totp-circle inner"
|
||||
r="12.6"
|
||||
cy="16"
|
||||
cx="16"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
|
||||
></circle>
|
||||
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="totp-code tw-mr-2 tw-ml-2 tw-mt-1"
|
||||
title="{{ 'verificationCodeTotp' | i18n }}"
|
||||
>{{ totpCodeFormatted }}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-items-center tw-border-none tw-bg-transparent tw-text-primary-500"
|
||||
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="cipher.login.hasUris">
|
||||
<div
|
||||
role="group"
|
||||
class="row"
|
||||
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
|
||||
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
|
||||
>
|
||||
<div class="col-7 form-group">
|
||||
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="form-control"
|
||||
id="loginUri{{ i }}"
|
||||
type="text"
|
||||
name="Login.Uris[{{ i }}].Uri"
|
||||
[(ngModel)]="u.uri"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
placeholder="{{ 'ex' | i18n }} https://google.com"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
(click)="launch(u)"
|
||||
[disabled]="!u.canLaunch"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyUri' | i18n }}"
|
||||
(click)="copy(u.uri, 'uri', 'URI')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="loginUriMatch{{ i }}">
|
||||
{{ "matchDetection" | i18n }}
|
||||
</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/uri-match-detection/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<select
|
||||
class="form-control overflow-hidden"
|
||||
id="loginUriMatch{{ i }}"
|
||||
name="Login.Uris[{{ i }}].Match"
|
||||
[(ngModel)]="u.match"
|
||||
(change)="loginUriMatchChanged(u)"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-danger ml-2"
|
||||
(click)="removeUri(u)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted && !viewOnly"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="addUri()"
|
||||
class="d-inline-block mb-3"
|
||||
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "newUri" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<!-- Card -->
|
||||
<ng-container *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
|
||||
<input
|
||||
id="cardCardholderName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Card.CardCardholderName"
|
||||
[(ngModel)]="cipher.card.cardholderName"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardBrand">{{ "brand" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
|
||||
<select
|
||||
id="cardBrand"
|
||||
class="form-control"
|
||||
name="Card.Brand"
|
||||
[(ngModel)]="cipher.card.brand"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardBrand>
|
||||
<input
|
||||
id="cardBrand"
|
||||
class="form-control"
|
||||
name="Card.Brand"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.card.brand"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardNumber">{{ "number" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="cardNumber"
|
||||
class="form-control text-monospace"
|
||||
type="{{ showCardNumber ? 'text' : 'password' }}"
|
||||
name="Card.Number"
|
||||
[(ngModel)]="cipher.card.number"
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardNumber()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-eye': !showCardNumber,
|
||||
'bwi-eye-slash': showCardNumber
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Number')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form-group">
|
||||
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
|
||||
<select
|
||||
id="cardExpMonth"
|
||||
class="form-control"
|
||||
name="Card.ExpMonth"
|
||||
[(ngModel)]="cipher.card.expMonth"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardExpMonth>
|
||||
<input
|
||||
id="cardExpMonth"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Card.ExpMonth"
|
||||
[readonly]="true"
|
||||
[value]="getCardExpMonthDisplay()"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="col form-group">
|
||||
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
|
||||
<input
|
||||
id="cardExpYear"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear"
|
||||
placeholder="{{ 'ex' | i18n }} 2019"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardCode">{{ "securityCode" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="cardCode"
|
||||
class="form-control text-monospace"
|
||||
type="{{ showCardCode ? 'text' : 'password' }}"
|
||||
name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code"
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardCode()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'securityCode' | i18n }}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Identity -->
|
||||
<ng-container *ngIf="cipher.type === cipherType.Identity">
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idTitle">{{ "title" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
|
||||
<select
|
||||
id="idTitle"
|
||||
class="form-control"
|
||||
name="Identity.Title"
|
||||
[(ngModel)]="cipher.identity.title"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyIdTitle>
|
||||
<input
|
||||
id="idTitle"
|
||||
class="form-control"
|
||||
name="Identity.Title"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.identity.title"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idFirstName">{{ "firstName" | i18n }}</label>
|
||||
<input
|
||||
id="idFirstName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
|
||||
<input
|
||||
id="idMiddleName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLastName">{{ "lastName" | i18n }}</label>
|
||||
<input
|
||||
id="idLastName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="idUsername"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idCompany">{{ "company" | i18n }}</label>
|
||||
<input
|
||||
id="idCompany"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idSsn">{{ "ssn" | i18n }}</label>
|
||||
<input
|
||||
id="idSsn"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idPassportNumber"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idLicenseNumber"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idEmail">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="idEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
inputmode="email"
|
||||
name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email"
|
||||
appInputVerbatim
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPhone">{{ "phone" | i18n }}</label>
|
||||
<input
|
||||
id="idPhone"
|
||||
class="form-control"
|
||||
type="text"
|
||||
inputmode="tel"
|
||||
name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress1">{{ "address1" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress1"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress2">{{ "address2" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress2"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress3">{{ "address3" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress3"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCity">{{ "cityTown" | i18n }}</label>
|
||||
<input
|
||||
id="idCity"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idState">{{ "stateProvince" | i18n }}</label>
|
||||
<input
|
||||
id="idState"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
|
||||
<input
|
||||
id="idPostalCode"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCountry">{{ "country" | i18n }}</label>
|
||||
<input
|
||||
id="idCountry"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{ "notes" | i18n }}</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
name="Notes"
|
||||
rows="6"
|
||||
[(ngModel)]="cipher.notes"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
<app-vault-add-edit-custom-fields
|
||||
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
|
||||
[cipher]="cipher"
|
||||
[thisCipherType]="cipher.type"
|
||||
[viewOnly]="viewOnly"
|
||||
[copy]="copy.bind(this)"
|
||||
[editMode]="editMode"
|
||||
></app-vault-add-edit-custom-fields>
|
||||
<ng-container *ngIf="allowOwnershipAssignment()">
|
||||
<h3 class="mt-4">{{ "ownership" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
|
||||
<select
|
||||
id="organizationId"
|
||||
class="form-control"
|
||||
name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId"
|
||||
(change)="organizationChanged()"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
>
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(!editMode || cloneMode) && cipher.organizationId">
|
||||
<h3 class="mt-4">{{ "collections" | i18n }}</h3>
|
||||
<div *ngIf="!collections || !collections.length">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
<ng-container *ngIf="collections && collections.length">
|
||||
<div class="form-check" *ngFor="let c of collections; let i = index">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
id="collection-{{ i }}"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
[disabled]="cipher.isDeleted || viewOnly"
|
||||
/>
|
||||
<label class="form-check-label" for="collection-{{ i }}">{{ c.name }}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="editMode">
|
||||
<div class="small text-muted mt-4">
|
||||
<div>
|
||||
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
|
||||
{{ cipher.revisionDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.creationDate">
|
||||
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
|
||||
{{ cipher.creationDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="showRevisionDate">
|
||||
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
|
||||
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="hasPasswordHistory">
|
||||
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
|
||||
<a href="#" appStopClick (click)="viewHistory()" title="{{ 'view' | i18n }}">
|
||||
{{ cipher.passwordHistory.length }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-3" *ngIf="viewingPasswordHistory">
|
||||
<div *ngFor="let ph of cipher.passwordHistory">
|
||||
{{ ph.lastUsedDate | date: "short" }} -
|
||||
<bit-color-password [password]="ph.password"></bit-color-password>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="canUseReprompt">
|
||||
<h3 class="mt-4">{{ "options" | i18n }}</h3>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
[ngModel]="reprompt"
|
||||
(change)="repromptChanged()"
|
||||
id="passwordPrompt"
|
||||
name="passwordPrompt"
|
||||
[disabled]="cipher.isDeleted || viewOnly || (!cipher.edit && editMode)"
|
||||
/>
|
||||
<label class="form-check-label" for="passwordPrompt">{{
|
||||
"passwordPrompt" | i18n
|
||||
}}</label>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!viewOnly"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ (cipher?.isDeleted ? "restore" : "save") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ (viewOnly ? "close" : "cancel") | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto" *ngIf="cipher && !viewOnly">
|
||||
<button
|
||||
*ngIf="!organization && !cipher.isDeleted"
|
||||
type="button"
|
||||
(click)="toggleFavorite()"
|
||||
class="btn btn-link"
|
||||
appA11yTitle="{{ (cipher.favorite ? 'unfavorite' : 'favorite') | i18n }}"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
[ngClass]="{ 'bwi-star-f': cipher.favorite, 'bwi-star': !cipher.favorite }"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="btn btn-outline-danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
*ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
[appApiAction]="deletePromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,256 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/components/add-edit.component";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { EventType } from "@bitwarden/common/enums/eventType";
|
||||
import { LoginUriView } from "@bitwarden/common/models/view/login-uri.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnDestroy {
|
||||
canAccessPremium: boolean;
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
totpSec: number;
|
||||
totpLow: boolean;
|
||||
showRevisionDate = false;
|
||||
hasPasswordHistory = false;
|
||||
viewingPasswordHistory = false;
|
||||
viewOnly = false;
|
||||
showPasswordCount = false;
|
||||
|
||||
protected totpInterval: number;
|
||||
protected override componentName = "app-vault-add-edit";
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
stateService: StateService,
|
||||
collectionService: CollectionService,
|
||||
protected totpService: TotpService,
|
||||
protected passwordGenerationService: PasswordGenerationService,
|
||||
protected messagingService: MessagingService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
protected policyService: PolicyService,
|
||||
organizationService: OrganizationService,
|
||||
logService: LogService,
|
||||
passwordRepromptService: PasswordRepromptService
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
stateService,
|
||||
collectionService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
toggleFavorite() {
|
||||
this.cipher.favorite = !this.cipher.favorite;
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
super.togglePassword();
|
||||
|
||||
// Hide password count when password is hidden to be safe
|
||||
if (!this.showPassword && this.showPasswordCount) {
|
||||
this.togglePasswordCount();
|
||||
}
|
||||
}
|
||||
|
||||
togglePasswordCount() {
|
||||
this.showPasswordCount = !this.showPasswordCount;
|
||||
|
||||
if (this.editMode && this.showPasswordCount) {
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledPasswordVisible,
|
||||
this.cipherId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
launch(uri: LoginUriView) {
|
||||
if (!uri.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
}
|
||||
|
||||
copy(value: string, typeI18nKey: string, aType: string) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||
this.cipherId
|
||||
);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
|
||||
} else if (aType === "H_Field") {
|
||||
this.eventCollectionService.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,
|
||||
});
|
||||
}
|
||||
|
||||
showGetPremium() {
|
||||
if (this.canAccessPremium) {
|
||||
return;
|
||||
}
|
||||
if (this.cipher.organizationUseTotp) {
|
||||
this.upgradeOrganization();
|
||||
} else {
|
||||
this.premiumRequired();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="attachmentsTitle">
|
||||
{{ "attachments" | i18n }}
|
||||
<small *ngIf="cipher">{{ cipher.name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</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="bwi bwi-fw bwi-lg bwi-file"
|
||||
*ngIf="!$any(a).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-lg bwi-fw bwi-spin"
|
||||
*ngIf="$any(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://bitwarden.com/help/attachments/#fixing-old-attachments"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-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]="$any(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]="$any(deleteBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!viewOnly"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-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>
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { AttachmentView } from "@bitwarden/common/models/view/attachment.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-attachments",
|
||||
templateUrl: "attachments.component.html",
|
||||
})
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
viewOnly = false;
|
||||
protected override componentName = "app-vault-attachments";
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
cryptoService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ (permanent ? "permanentlyDeleteSelected" : "deleteSelected") | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<ng-container *ngIf="!permanent">
|
||||
<span *ngIf="cipherIds?.length">
|
||||
{{ "deleteSelectedItemsDesc" | i18n: cipherIds.length }}
|
||||
</span>
|
||||
<span *ngIf="collectionIds?.length">
|
||||
{{ "deleteSelectedCollectionsDesc" | i18n: collectionIds.length }}
|
||||
</span>
|
||||
{{ "deleteSelectedConfirmation" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="permanent">
|
||||
{{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length }}
|
||||
</ng-container>
|
||||
</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton type="submit" buttonType="danger" [bitAction]="submit">
|
||||
{{ (permanent ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
</bit-simple-dialog>
|
||||
@@ -1,134 +0,0 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { CipherBulkDeleteRequest } from "@bitwarden/common/models/request/cipher-bulk-delete.request";
|
||||
import { CollectionBulkDeleteRequest } from "@bitwarden/common/models/request/collection-bulk-delete.request";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkDeleteDialogParams {
|
||||
cipherIds?: string[];
|
||||
collectionIds?: string[];
|
||||
permanent?: boolean;
|
||||
organization?: Organization;
|
||||
}
|
||||
|
||||
export enum BulkDeleteDialogResult {
|
||||
Deleted = "deleted",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a BulkDeleteDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openBulkDeleteDialog = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkDeleteDialogParams>
|
||||
) => {
|
||||
return dialogService.open<BulkDeleteDialogResult, BulkDeleteDialogParams>(
|
||||
BulkDeleteDialogComponent,
|
||||
config
|
||||
);
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "vault-bulk-delete-dialog",
|
||||
templateUrl: "bulk-delete-dialog.component.html",
|
||||
})
|
||||
export class BulkDeleteDialogComponent {
|
||||
cipherIds: string[];
|
||||
collectionIds: string[];
|
||||
permanent = false;
|
||||
organization: Organization;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
this.collectionIds = params.collectionIds ?? [];
|
||||
this.permanent = params.permanent;
|
||||
this.organization = params.organization;
|
||||
}
|
||||
|
||||
protected async cancel() {
|
||||
this.close(BulkDeleteDialogResult.Canceled);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
if (this.cipherIds.length) {
|
||||
if (!this.organization || !this.organization.canEditAnyCollection) {
|
||||
deletePromises.push(this.deleteCiphers());
|
||||
} else {
|
||||
deletePromises.push(this.deleteCiphersAdmin());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.collectionIds.length && this.organization) {
|
||||
deletePromises.push(this.deleteCollections());
|
||||
}
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
if (this.cipherIds.length) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "deletedItems")
|
||||
);
|
||||
}
|
||||
if (this.collectionIds.length) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("deletedCollections")
|
||||
);
|
||||
}
|
||||
this.close(BulkDeleteDialogResult.Deleted);
|
||||
};
|
||||
|
||||
private async deleteCiphers(): Promise<any> {
|
||||
if (this.permanent) {
|
||||
await this.cipherService.deleteManyWithServer(this.cipherIds);
|
||||
} else {
|
||||
await this.cipherService.softDeleteManyWithServer(this.cipherIds);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCiphersAdmin(): Promise<any> {
|
||||
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
|
||||
if (this.permanent) {
|
||||
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
||||
} else {
|
||||
return await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCollections(): Promise<any> {
|
||||
if (!this.organization.canDeleteAssignedCollections) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("missingPermissions")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const deleteRequest = new CollectionBulkDeleteRequest(this.collectionIds, this.organization.id);
|
||||
return await this.apiService.deleteManyCollections(deleteRequest);
|
||||
}
|
||||
|
||||
private close(result: BulkDeleteDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { BulkDeleteDialogComponent } from "./bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import { BulkMoveDialogComponent } from "./bulk-move-dialog/bulk-move-dialog.component";
|
||||
import { BulkRestoreDialogComponent } from "./bulk-restore-dialog/bulk-restore-dialog.component";
|
||||
import { BulkShareDialogComponent } from "./bulk-share-dialog/bulk-share-dialog.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [
|
||||
BulkDeleteDialogComponent,
|
||||
BulkMoveDialogComponent,
|
||||
BulkRestoreDialogComponent,
|
||||
BulkShareDialogComponent,
|
||||
],
|
||||
exports: [
|
||||
BulkDeleteDialogComponent,
|
||||
BulkMoveDialogComponent,
|
||||
BulkRestoreDialogComponent,
|
||||
BulkShareDialogComponent,
|
||||
],
|
||||
})
|
||||
export class BulkDialogsModule {}
|
||||
@@ -1,24 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="small">
|
||||
<span bitDialogTitle>
|
||||
{{ "moveSelected" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label for="folder">{{ "folder" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="folderId">
|
||||
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,85 +0,0 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkMoveDialogParams {
|
||||
cipherIds?: string[];
|
||||
}
|
||||
|
||||
export enum BulkMoveDialogResult {
|
||||
Moved = "moved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a BulkMoveDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openBulkMoveDialog = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkMoveDialogParams>
|
||||
) => {
|
||||
return dialogService.open<BulkMoveDialogResult, BulkMoveDialogParams>(
|
||||
BulkMoveDialogComponent,
|
||||
config
|
||||
);
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "vault-bulk-move-dialog",
|
||||
templateUrl: "bulk-move-dialog.component.html",
|
||||
})
|
||||
export class BulkMoveDialogComponent implements OnInit {
|
||||
cipherIds: string[] = [];
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
folderId: ["", [Validators.required]],
|
||||
});
|
||||
folders$: Observable<FolderView[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
|
||||
private dialogRef: DialogRef<BulkMoveDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private folderService: FolderService,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.folders$ = this.folderService.folderViews$;
|
||||
this.formGroup.patchValue({
|
||||
folderId: (await firstValueFrom(this.folders$))[0].id,
|
||||
});
|
||||
}
|
||||
|
||||
protected cancel() {
|
||||
this.close(BulkMoveDialogResult.Canceled);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.cipherService.moveManyWithServer(this.cipherIds, this.formGroup.value.folderId);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("movedItems"));
|
||||
this.close(BulkMoveDialogResult.Moved);
|
||||
};
|
||||
|
||||
private close(result: BulkMoveDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "restoreSelected" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
{{ "restoreSelectedItemsDesc" | i18n: cipherIds.length }}
|
||||
</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton type="submit" buttonType="primary" [bitAction]="submit">
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
</bit-simple-dialog>
|
||||
@@ -1,63 +0,0 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkRestoreDialogParams {
|
||||
cipherIds: string[];
|
||||
}
|
||||
|
||||
export enum BulkRestoreDialogResult {
|
||||
Restored = "restored",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a BulkRestoreDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openBulkRestoreDialog = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkRestoreDialogParams>
|
||||
) => {
|
||||
return dialogService.open<BulkRestoreDialogResult, BulkRestoreDialogParams>(
|
||||
BulkRestoreDialogComponent,
|
||||
config
|
||||
);
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "vault-bulk-restore-dialog",
|
||||
templateUrl: "bulk-restore-dialog.component.html",
|
||||
})
|
||||
export class BulkRestoreDialogComponent {
|
||||
cipherIds: string[];
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkRestoreDialogParams,
|
||||
private dialogRef: DialogRef<BulkRestoreDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
await this.cipherService.restoreManyWithServer(this.cipherIds);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
|
||||
this.close(BulkRestoreDialogResult.Restored);
|
||||
};
|
||||
|
||||
protected cancel() {
|
||||
this.close(BulkRestoreDialogResult.Canceled);
|
||||
}
|
||||
|
||||
private close(result: BulkRestoreDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "moveSelectedToOrg" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<p>{{ "moveManyToOrgDesc" | i18n }}</p>
|
||||
<p>
|
||||
{{
|
||||
"moveSelectedItemsCountDesc"
|
||||
| i18n: this.ciphers.length:shareableCiphers.length:nonShareableCount
|
||||
}}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label for="organization">{{ "organization" | i18n }}</bit-label>
|
||||
<select
|
||||
bitInput
|
||||
[(ngModel)]="organizationId"
|
||||
id="organization"
|
||||
(change)="filterCollections()"
|
||||
>
|
||||
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="d-flex">
|
||||
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{
|
||||
"collections" | i18n
|
||||
}}</label>
|
||||
<div class="tw-ml-auto tw-flex tw-gap-2" *ngIf="collections && collections.length">
|
||||
<button bitLink type="button" (click)="selectAll(true)" class="tw-px-2">
|
||||
{{ "selectAll" | i18n }}
|
||||
</button>
|
||||
<button bitLink type="button" (click)="selectAll(false)" class="tw-px-2">
|
||||
{{ "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"
|
||||
id="collections"
|
||||
>
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||
<td class="table-list-checkbox">
|
||||
<input
|
||||
bitInput
|
||||
type="checkbox"
|
||||
[(ngModel)]="c.checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
attr.aria-label="Check {{ c.name }}"
|
||||
appStopProp
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{ c.name }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton type="submit" buttonType="primary" [bitAction]="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
@@ -1,152 +0,0 @@
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkShareDialogParams {
|
||||
ciphers: CipherView[];
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export enum BulkShareDialogResult {
|
||||
Shared = "shared",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a BulkShareDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openBulkShareDialog = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkShareDialogParams>
|
||||
) => {
|
||||
return dialogService.open<BulkShareDialogResult, BulkShareDialogParams>(
|
||||
BulkShareDialogComponent,
|
||||
config
|
||||
);
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "vault-bulk-share-dialog",
|
||||
templateUrl: "bulk-share-dialog.component.html",
|
||||
})
|
||||
export class BulkShareDialogComponent implements OnInit {
|
||||
ciphers: CipherView[] = [];
|
||||
organizationId: string;
|
||||
|
||||
nonShareableCount = 0;
|
||||
collections: Checkable<CollectionView>[] = [];
|
||||
organizations: Organization[] = [];
|
||||
shareableCiphers: CipherView[] = [];
|
||||
|
||||
private writeableCollections: CollectionView[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkShareDialogParams,
|
||||
private dialogRef: DialogRef<BulkShareDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private logService: LogService
|
||||
) {
|
||||
this.ciphers = params.ciphers ?? [];
|
||||
this.organizationId = params.organizationId;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const checkedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
|
||||
try {
|
||||
await this.cipherService.shareManyWithServer(
|
||||
this.shareableCiphers,
|
||||
this.organizationId,
|
||||
checkedCollectionIds
|
||||
);
|
||||
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)
|
||||
);
|
||||
this.close(BulkShareDialogResult.Shared);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
check(c: Checkable<CollectionView>, select?: boolean) {
|
||||
c.checked = select == null ? !c.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].checked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected cancel() {
|
||||
this.close(BulkShareDialogResult.Canceled);
|
||||
}
|
||||
|
||||
private close(result: BulkShareDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="collectionsTitle">
|
||||
{{ "collections" | i18n }}
|
||||
<small *ngIf="cipher">{{ cipher.name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</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)]="$any(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="bwi bwi-spinner bwi-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>
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
|
||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/components/collections.component";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
|
||||
@Component({
|
||||
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);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="folderAddEditTitle">{{ title }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</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="bwi bwi-spinner bwi-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]="$any(deleteBtn).loading"
|
||||
[appApiAction]="deletePromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-folder-add-edit",
|
||||
templateUrl: "folder-add-edit.component.html",
|
||||
})
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
|
||||
protected override componentName = "app-folder-add-edit";
|
||||
constructor(
|
||||
folderService: FolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService
|
||||
) {
|
||||
super(folderService, folderApiService, i18nService, platformUtilsService, logService);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [OrganizationNameBadgeComponent],
|
||||
exports: [OrganizationNameBadgeComponent],
|
||||
})
|
||||
export class OrganizationBadgeModule {}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<button
|
||||
bitBadge
|
||||
[style.color]="textColor"
|
||||
[style.background-color]="color"
|
||||
appA11yTitle="{{ organizationName }}"
|
||||
(click)="emitOnOrganizationClicked()"
|
||||
>
|
||||
{{ organizationName | ellipsis: 13 }}
|
||||
</button>
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-badge",
|
||||
templateUrl: "organization-name-badge.component.html",
|
||||
})
|
||||
export class OrganizationNameBadgeComponent implements OnInit {
|
||||
@Input() organizationName: string;
|
||||
@Input() profileName: string;
|
||||
|
||||
@Output() onOrganizationClicked = new EventEmitter<string>();
|
||||
|
||||
color: string;
|
||||
textColor: string;
|
||||
isMe: boolean;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private avatarService: AvatarUpdateService,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.organizationName == null || this.organizationName === "") {
|
||||
this.organizationName = this.i18nService.t("me");
|
||||
this.isMe = true;
|
||||
}
|
||||
if (this.isMe) {
|
||||
this.color = await this.avatarService.loadColorFromState();
|
||||
if (this.color == null) {
|
||||
const userName =
|
||||
(await this.tokenService.getName()) ?? (await this.tokenService.getEmail());
|
||||
this.color = Utils.stringToColor(userName.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
this.color = Utils.stringToColor(this.organizationName.toUpperCase());
|
||||
}
|
||||
this.textColor = Utils.pickTextColorBasedOnBgColor(this.color, 135, true) + "!important";
|
||||
}
|
||||
|
||||
emitOnOrganizationClicked() {
|
||||
this.onOrganizationClicked.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/common/src/models/view/collection.view";
|
||||
|
||||
@Pipe({
|
||||
name: "collectionNameFromId",
|
||||
pure: true,
|
||||
})
|
||||
export class GetCollectionNameFromIdPipe implements PipeTransform {
|
||||
transform(value: string, collections: CollectionView[]) {
|
||||
return collections.find((o) => o.id === value)?.name;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { GroupView } from "../../organizations/core";
|
||||
|
||||
@Pipe({
|
||||
name: "groupNameFromId",
|
||||
pure: true,
|
||||
})
|
||||
export class GetGroupNameFromIdPipe implements PipeTransform {
|
||||
transform(value: string, groups: GroupView[]) {
|
||||
return groups.find((o) => o.id === value)?.name;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
@Pipe({
|
||||
name: "orgNameFromId",
|
||||
pure: true,
|
||||
})
|
||||
export class GetOrgNameFromIdPipe implements PipeTransform {
|
||||
transform(value: string, organizations: Organization[]) {
|
||||
const orgName = organizations.find((o) => o.id === value)?.name;
|
||||
return orgName;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { GetCollectionNameFromIdPipe } from "./get-collection-name.pipe";
|
||||
import { GetGroupNameFromIdPipe } from "./get-group-name.pipe";
|
||||
import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe";
|
||||
|
||||
@NgModule({
|
||||
declarations: [GetOrgNameFromIdPipe, GetCollectionNameFromIdPipe, GetGroupNameFromIdPipe],
|
||||
exports: [GetOrgNameFromIdPipe, GetCollectionNameFromIdPipe, GetGroupNameFromIdPipe],
|
||||
})
|
||||
export class PipesModule {}
|
||||
@@ -1,101 +0,0 @@
|
||||
<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">
|
||||
<h1 class="modal-title" id="shareTitle">
|
||||
{{ "moveToOrganization" | i18n }}
|
||||
<small *ngIf="cipher">{{ cipher.name }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<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="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
routerLink="/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>
|
||||
</ng-container>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
|
||||
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
|
||||
@Component({
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<a
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main tw-no-underline hover:tw-bg-secondary-100 hover:tw-no-underline focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="submit(returnUri, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
{{ "linkSso" | i18n }}
|
||||
</a>
|
||||
@@ -1,59 +0,0 @@
|
||||
import { AfterContentInit, Component, Input } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SsoComponent } from "@bitwarden/angular/components/sso.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
@Component({
|
||||
selector: "app-link-sso",
|
||||
templateUrl: "link-sso.component.html",
|
||||
})
|
||||
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
|
||||
@Input() organization: Organization;
|
||||
returnUri = "/settings/organizations";
|
||||
|
||||
constructor(
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
apiService: ApiService,
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
route: ActivatedRoute,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationService,
|
||||
stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService
|
||||
) {
|
||||
super(
|
||||
authService,
|
||||
router,
|
||||
i18nService,
|
||||
route,
|
||||
stateService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
logService
|
||||
);
|
||||
|
||||
this.returnUri = "/settings/organizations";
|
||||
this.redirectUri = window.location.origin + "/sso-connector.html";
|
||||
this.clientId = "web";
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
this.identifier = this.organization.identifier;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted tw-m-2"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="loaded"
|
||||
class="tw-flex tw-min-w-[200px] tw-max-w-[300px] tw-flex-col"
|
||||
[appApiAction]="actionPromise"
|
||||
>
|
||||
<button
|
||||
*ngIf="allowEnrollmentChanges(organization) && !organization.resetPasswordEnrolled"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "enrollPasswordReset" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="allowEnrollmentChanges(organization) && organization.resetPasswordEnrolled"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "withdrawPasswordReset" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="organization.useSso && organization.identifier">
|
||||
<button
|
||||
*ngIf="organization.ssoBound; else linkSso"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="unlinkSso(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<app-link-sso [organization]="organization"> </app-link-sso>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<button
|
||||
class="text-danger tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="leave(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||
{{ "leave" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,157 +0,0 @@
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/models/domain/policy";
|
||||
|
||||
import { EnrollMasterPasswordReset } from "../../../organizations/users/enroll-master-password-reset.component";
|
||||
import { OptionsInput } from "../shared/components/vault-filter-section.component";
|
||||
import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-options",
|
||||
templateUrl: "organization-options.component.html",
|
||||
})
|
||||
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
actionPromise: Promise<void | boolean>;
|
||||
policies: Policy[];
|
||||
loaded = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(OptionsInput) protected organization: OrganizationFilter,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private policyService: PolicyService,
|
||||
private modalService: ModalService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.policyService.policies$
|
||||
.pipe(
|
||||
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe((policies) => {
|
||||
this.policies = policies;
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
allowEnrollmentChanges(org: OrganizationFilter): boolean {
|
||||
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
|
||||
const policy = this.policies.find((p) => p.organizationId === org.id);
|
||||
if (policy != null && policy.enabled) {
|
||||
return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
showEnrolledStatus(org: Organization): boolean {
|
||||
return (
|
||||
org.useResetPassword &&
|
||||
org.resetPasswordEnrolled &&
|
||||
this.policies.some((p) => p.organizationId === org.id && p.enabled)
|
||||
);
|
||||
}
|
||||
|
||||
async unlinkSso(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("unlinkSsoConfirmation"),
|
||||
org.name,
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async leave(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("leaveOrganizationConfirmation"),
|
||||
org.name,
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.organizationApiService.leave(org.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleResetPasswordEnrollment(org: Organization) {
|
||||
if (!this.organization.resetPasswordEnrolled) {
|
||||
this.modalService.open(EnrollMasterPasswordReset, {
|
||||
allowMultipleModals: true,
|
||||
data: {
|
||||
organization: org,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Remove reset password
|
||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
request.masterPasswordHash = "ignored";
|
||||
request.resetPasswordKey = null;
|
||||
this.actionPromise = this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.organization.id,
|
||||
this.organization.userId,
|
||||
request
|
||||
);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("withdrawPasswordResetSuccess")
|
||||
);
|
||||
this.syncService.fullSync(true);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<div class="card vault-filters">
|
||||
<div class="container loading-spinner" *ngIf="!isLoaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div *ngIf="isLoaded">
|
||||
<div class="card-header d-flex">
|
||||
{{ "filters" | i18n }}
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/searching-vault/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="{{ searchPlaceholder | i18n }}"
|
||||
id="search"
|
||||
class="form-control"
|
||||
(input)="searchTextChanged($any($event.target).value)"
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
/>
|
||||
<ng-container *ngFor="let f of filtersList">
|
||||
<div class="filter">
|
||||
<app-filter-section [activeFilter]="activeFilter" [section]="f"> </app-filter-section>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,353 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
|
||||
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterList,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../shared/models/vault-filter-section.type";
|
||||
import { VaultFilter } from "../shared/models/vault-filter.model";
|
||||
import {
|
||||
CipherStatus,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../shared/models/vault-filter.type";
|
||||
|
||||
import { OrganizationOptionsComponent } from "./organization-options.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
filters?: VaultFilterList;
|
||||
@Input() activeFilter: VaultFilter = new VaultFilter();
|
||||
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
|
||||
@Output() onSearchTextChanged = new EventEmitter<string>();
|
||||
@Output() onAddFolder = new EventEmitter<never>();
|
||||
@Output() onEditFolder = new EventEmitter<FolderFilter>();
|
||||
|
||||
isLoaded = false;
|
||||
searchText = "";
|
||||
|
||||
protected destroy$: Subject<void> = new Subject<void>();
|
||||
|
||||
get filtersList() {
|
||||
return this.filters ? Object.values(this.filters) : [];
|
||||
}
|
||||
|
||||
get searchPlaceholder() {
|
||||
if (this.activeFilter.isFavorites) {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (this.activeFilter.isDeleted) {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (this.activeFilter.cipherType === CipherType.Login) {
|
||||
return "searchLogin";
|
||||
}
|
||||
if (this.activeFilter.cipherType === CipherType.Card) {
|
||||
return "searchCard";
|
||||
}
|
||||
if (this.activeFilter.cipherType === CipherType.Identity) {
|
||||
return "searchIdentity";
|
||||
}
|
||||
if (this.activeFilter.cipherType === CipherType.SecureNote) {
|
||||
return "searchSecureNote";
|
||||
}
|
||||
if (this.activeFilter.selectedFolderNode?.node) {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (this.activeFilter.selectedCollectionNode?.node) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (this.activeFilter.organizationId === "MyVault") {
|
||||
return "searchMyVault";
|
||||
}
|
||||
if (this.activeFilter.organizationId) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
|
||||
return "searchVault";
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected vaultFilterService: VaultFilterService,
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService
|
||||
) {
|
||||
this.loadSubscriptions();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.filters = await this.buildAllFilters();
|
||||
this.activeFilter.selectedCipherTypeNode =
|
||||
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
protected loadSubscriptions() {
|
||||
this.vaultFilterService.filteredFolders$
|
||||
.pipe(
|
||||
switchMap(async (folders) => {
|
||||
this.removeInvalidFolderSelection(folders);
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.vaultFilterService.filteredCollections$
|
||||
.pipe(
|
||||
switchMap(async (collections) => {
|
||||
this.removeInvalidCollectionSelection(collections);
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
searchTextChanged(t: string) {
|
||||
this.searchText = t;
|
||||
this.onSearchTextChanged.emit(t);
|
||||
}
|
||||
|
||||
protected applyVaultFilter(filter: VaultFilter) {
|
||||
this.activeFilterChanged.emit(filter);
|
||||
}
|
||||
|
||||
applyOrganizationFilter = async (orgNode: TreeNode<OrganizationFilter>): Promise<void> => {
|
||||
if (!orgNode?.node.enabled) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("disabledOrganizationFilterError")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const filter = this.activeFilter;
|
||||
filter.resetOrganization();
|
||||
if (orgNode?.node.id !== "AllVaults") {
|
||||
filter.selectedOrganizationNode = orgNode;
|
||||
}
|
||||
this.vaultFilterService.setOrganizationFilter(orgNode.node);
|
||||
await this.vaultFilterService.expandOrgFilter();
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
applyTypeFilter = async (filterNode: TreeNode<CipherTypeFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCipherTypeNode = filterNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
applyFolderFilter = async (folderNode: TreeNode<FolderFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedFolderNode = folderNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
applyCollectionFilter = async (collectionNode: TreeNode<CollectionFilter>): Promise<void> => {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCollectionNode = collectionNode;
|
||||
this.applyVaultFilter(filter);
|
||||
};
|
||||
|
||||
addFolder = async (): Promise<void> => {
|
||||
this.onAddFolder.emit();
|
||||
};
|
||||
|
||||
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||
this.onEditFolder.emit(folder);
|
||||
};
|
||||
|
||||
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
|
||||
return await firstValueFrom(this.filters?.typeFilter.data$);
|
||||
}
|
||||
|
||||
protected async removeInvalidFolderSelection(folders: FolderView[]) {
|
||||
if (this.activeFilter.selectedFolderNode) {
|
||||
if (!folders.some((f) => f.id === this.activeFilter.folderId)) {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCipherTypeNode =
|
||||
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
|
||||
this.applyVaultFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async removeInvalidCollectionSelection(collections: CollectionView[]) {
|
||||
if (this.activeFilter.selectedCollectionNode) {
|
||||
if (!collections.some((f) => f.id === this.activeFilter.collectionId)) {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCipherTypeNode =
|
||||
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
|
||||
this.applyVaultFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||
builderFilter.typeFilter = await this.addTypeFilter();
|
||||
builderFilter.folderFilter = await this.addFolderFilter();
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
}
|
||||
|
||||
protected async addOrganizationFilter(): Promise<VaultFilterSection> {
|
||||
const singleOrgPolicy = await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
|
||||
const personalVaultPolicy = await this.policyService.policyAppliesToUser(
|
||||
PolicyType.PersonalOwnership
|
||||
);
|
||||
|
||||
const addAction = !singleOrgPolicy
|
||||
? { text: "newOrganization", route: "/create-organization" }
|
||||
: null;
|
||||
|
||||
const orgFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.organizationTree$,
|
||||
header: {
|
||||
showHeader: !(singleOrgPolicy && personalVaultPolicy),
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyOrganizationFilter,
|
||||
options: { component: OrganizationOptionsComponent },
|
||||
add: addAction,
|
||||
divider: true,
|
||||
};
|
||||
|
||||
return orgFilterSection;
|
||||
}
|
||||
|
||||
protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise<VaultFilterSection> {
|
||||
const allTypeFilters: CipherTypeFilter[] = [
|
||||
{
|
||||
id: "favorites",
|
||||
name: this.i18nService.t("favorites"),
|
||||
type: "favorites",
|
||||
icon: "bwi-star",
|
||||
},
|
||||
{
|
||||
id: "login",
|
||||
name: this.i18nService.t("typeLogin"),
|
||||
type: CipherType.Login,
|
||||
icon: "bwi-globe",
|
||||
},
|
||||
{
|
||||
id: "card",
|
||||
name: this.i18nService.t("typeCard"),
|
||||
type: CipherType.Card,
|
||||
icon: "bwi-credit-card",
|
||||
},
|
||||
{
|
||||
id: "identity",
|
||||
name: this.i18nService.t("typeIdentity"),
|
||||
type: CipherType.Identity,
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
{
|
||||
id: "note",
|
||||
name: this.i18nService.t("typeSecureNote"),
|
||||
type: CipherType.SecureNote,
|
||||
icon: "bwi-sticky-note",
|
||||
},
|
||||
];
|
||||
|
||||
const typeFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.buildTypeTree(
|
||||
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
|
||||
allTypeFilters.filter((f) => !excludeTypes.includes(f.type))
|
||||
),
|
||||
header: {
|
||||
showHeader: true,
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyTypeFilter,
|
||||
};
|
||||
return typeFilterSection;
|
||||
}
|
||||
|
||||
protected async addFolderFilter(): Promise<VaultFilterSection> {
|
||||
const folderFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.folderTree$,
|
||||
header: {
|
||||
showHeader: true,
|
||||
isSelectable: false,
|
||||
},
|
||||
action: this.applyFolderFilter,
|
||||
edit: {
|
||||
text: "editFolder",
|
||||
action: this.editFolder,
|
||||
},
|
||||
add: {
|
||||
text: "Add Folder",
|
||||
action: this.addFolder,
|
||||
},
|
||||
};
|
||||
return folderFilterSection;
|
||||
}
|
||||
|
||||
protected async addCollectionFilter(): Promise<VaultFilterSection> {
|
||||
const collectionFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.collectionTree$,
|
||||
header: {
|
||||
showHeader: true,
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyCollectionFilter,
|
||||
};
|
||||
return collectionFilterSection;
|
||||
}
|
||||
|
||||
protected async addTrashFilter(): Promise<VaultFilterSection> {
|
||||
const trashFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.buildTypeTree(
|
||||
{
|
||||
id: "headTrash",
|
||||
name: "HeadTrash",
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
},
|
||||
[
|
||||
{
|
||||
id: "trash",
|
||||
name: this.i18nService.t("trash"),
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
},
|
||||
]
|
||||
),
|
||||
header: {
|
||||
showHeader: false,
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyTypeFilter,
|
||||
};
|
||||
return trashFilterSection;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/src/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/src/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/src/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/src/models/view/folder.view";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../../shared/models/vault-filter.type";
|
||||
|
||||
export abstract class VaultFilterService {
|
||||
collapsedFilterNodes$: Observable<Set<string>>;
|
||||
filteredFolders$: Observable<FolderView[]>;
|
||||
filteredCollections$: Observable<CollectionView[]>;
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>>;
|
||||
folderTree$: Observable<TreeNode<FolderFilter>>;
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>>;
|
||||
reloadCollections: () => Promise<void>;
|
||||
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
|
||||
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
|
||||
expandOrgFilter: () => Promise<void>;
|
||||
setOrganizationFilter: (organization: Organization) => void;
|
||||
buildTypeTree: (
|
||||
head: CipherTypeFilter,
|
||||
array: CipherTypeFilter[]
|
||||
) => Observable<TreeNode<CipherTypeFilter>>;
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, ReplaySubject, take } from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
describe("vault filter service", () => {
|
||||
let vaultFilterService: VaultFilterService;
|
||||
|
||||
let stateService: MockProxy<StateService>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let folderService: MockProxy<FolderService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let collectionService: MockProxy<CollectionService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let organizations: ReplaySubject<Organization[]>;
|
||||
let folderViews: ReplaySubject<FolderView[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
stateService = mock<StateService>();
|
||||
organizationService = mock<OrganizationService>();
|
||||
folderService = mock<FolderService>();
|
||||
cipherService = mock<CipherService>();
|
||||
collectionService = mock<CollectionService>();
|
||||
policyService = mock<PolicyService>();
|
||||
i18nService = mock<I18nService>();
|
||||
i18nService.collator = new Intl.Collator("en-US");
|
||||
|
||||
organizations = new ReplaySubject<Organization[]>(1);
|
||||
folderViews = new ReplaySubject<FolderView[]>(1);
|
||||
|
||||
organizationService.organizations$ = organizations;
|
||||
folderService.folderViews$ = folderViews;
|
||||
|
||||
vaultFilterService = new VaultFilterService(
|
||||
stateService,
|
||||
organizationService,
|
||||
folderService,
|
||||
cipherService,
|
||||
collectionService,
|
||||
policyService,
|
||||
i18nService
|
||||
);
|
||||
});
|
||||
|
||||
describe("collapsed filter nodes", () => {
|
||||
const nodes = new Set(["1", "2"]);
|
||||
it("updates observable when saving", (complete) => {
|
||||
vaultFilterService.collapsedFilterNodes$.pipe(take(1)).subscribe((value) => {
|
||||
if (value === nodes) {
|
||||
complete();
|
||||
}
|
||||
});
|
||||
|
||||
vaultFilterService.setCollapsedFilterNodes(nodes);
|
||||
});
|
||||
|
||||
it("loads from state on initialization", async () => {
|
||||
stateService.getCollapsedGroupings.mockResolvedValue(["1", "2"]);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.collapsedFilterNodes$)).resolves.toEqual(
|
||||
nodes
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizations", () => {
|
||||
beforeEach(() => {
|
||||
const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")];
|
||||
organizations.next(storedOrgs);
|
||||
});
|
||||
|
||||
it("returns a nested tree", async () => {
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(3);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
expect(tree.children.find((o) => o.node.name === "org2"));
|
||||
});
|
||||
|
||||
it("hides My Vault if personal ownership policy is enabled", async () => {
|
||||
policyService.policyAppliesToUser
|
||||
.calledWith(PolicyType.PersonalOwnership)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(2);
|
||||
expect(!tree.children.find((o) => o.node.id === "MyVault"));
|
||||
});
|
||||
|
||||
it("returns 1 organization and My Vault if single organization policy is enabled", async () => {
|
||||
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(2);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
expect(tree.children.find((o) => o.node.id === "MyVault"));
|
||||
});
|
||||
|
||||
it("returns 1 organization if both single organization and personal ownership policies are enabled", async () => {
|
||||
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
|
||||
policyService.policyAppliesToUser
|
||||
.calledWith(PolicyType.PersonalOwnership)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
|
||||
|
||||
expect(tree.children.length).toBe(1);
|
||||
expect(tree.children.find((o) => o.node.name === "org1"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("folders", () => {
|
||||
describe("filtered folders with organization", () => {
|
||||
beforeEach(() => {
|
||||
// Org must be updated before folderService else the subscription uses the null org default value
|
||||
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
|
||||
});
|
||||
it("returns folders filtered by current organization", async () => {
|
||||
const storedCiphers = [
|
||||
createCipherView("1", "org test id", "folder test id"),
|
||||
createCipherView("2", "non matching org id", "non matching folder id"),
|
||||
];
|
||||
cipherService.getAllDecrypted.mockResolvedValue(storedCiphers);
|
||||
|
||||
const storedFolders = [
|
||||
createFolderView("folder test id", "test"),
|
||||
createFolderView("non matching folder id", "test2"),
|
||||
];
|
||||
folderViews.next(storedFolders);
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toEqual([
|
||||
createFolderView("folder test id", "test"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("folder tree", () => {
|
||||
it("returns a nested tree", async () => {
|
||||
const storedFolders = [
|
||||
createFolderView("Folder 1 Id", "Folder 1"),
|
||||
createFolderView("Folder 2 Id", "Folder 1/Folder 2"),
|
||||
createFolderView("Folder 3 Id", "Folder 1/Folder 3"),
|
||||
];
|
||||
folderViews.next(storedFolders);
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.folderTree$);
|
||||
|
||||
expect(result.children[0].node.id === "Folder 1 Id");
|
||||
expect(result.children[0].children.find((c) => c.node.id === "Folder 2 Id"));
|
||||
expect(result.children[0].children.find((c) => c.node.id === "Folder 3 Id"));
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collections", () => {
|
||||
describe("filtered collections", () => {
|
||||
it("returns collections filtered by current organization", async () => {
|
||||
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
|
||||
|
||||
const storedCollections = [
|
||||
createCollectionView("1", "collection 1", "org test id"),
|
||||
createCollectionView("2", "collection 2", "non matching org id"),
|
||||
];
|
||||
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||
vaultFilterService.reloadCollections();
|
||||
|
||||
await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([
|
||||
createCollectionView("1", "collection 1", "org test id"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection tree", () => {
|
||||
it("returns tree with children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 3", "org test id"),
|
||||
];
|
||||
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||
vaultFilterService.reloadCollections();
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-2", "id-3"]);
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from children", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||
vaultFilterService.reloadCollections();
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
|
||||
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-3"]);
|
||||
expect(result.children[0].children[0].node.name).toBe("Collection 2/Collection 3");
|
||||
});
|
||||
|
||||
it("returns tree with parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
createCollectionView("id-4", "Collection 1/Collection 4", "org test id"),
|
||||
];
|
||||
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||
vaultFilterService.reloadCollections();
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c2 = c1.children[0];
|
||||
const c3 = c2.children[0];
|
||||
const c4 = c1.children[1];
|
||||
expect(c2.parent.node.id).toEqual("id-1");
|
||||
expect(c3.parent.node.id).toEqual("id-2");
|
||||
expect(c4.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
|
||||
it("returns tree where non-existing collections are excluded from parents", async () => {
|
||||
const storedCollections = [
|
||||
createCollectionView("id-1", "Collection 1", "org test id"),
|
||||
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
|
||||
];
|
||||
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
|
||||
vaultFilterService.reloadCollections();
|
||||
|
||||
const result = await firstValueFrom(vaultFilterService.collectionTree$);
|
||||
|
||||
const c1 = result.children[0];
|
||||
const c3 = c1.children[0];
|
||||
expect(c3.parent.node.id).toEqual("id-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createOrganization(id: string, name: string) {
|
||||
const org = new Organization();
|
||||
org.id = id;
|
||||
org.name = name;
|
||||
org.identifier = name;
|
||||
return org;
|
||||
}
|
||||
|
||||
function createCipherView(id: string, orgId: string, folderId: string) {
|
||||
const cipher = new CipherView();
|
||||
cipher.id = id;
|
||||
cipher.organizationId = orgId;
|
||||
cipher.folderId = folderId;
|
||||
return cipher;
|
||||
}
|
||||
|
||||
function createFolderView(id: string, name: string): FolderView {
|
||||
const folder = new FolderView();
|
||||
folder.id = id;
|
||||
folder.name = name;
|
||||
return folder;
|
||||
}
|
||||
|
||||
function createCollectionView(id: string, name: string, orgId: string): CollectionView {
|
||||
const collection = new CollectionView();
|
||||
collection.id = id;
|
||||
collection.name = name;
|
||||
collection.organizationId = orgId;
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatestWith,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
|
||||
import { CollectionAdminView } from "../../../organizations/core";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "../shared/models/vault-filter.type";
|
||||
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "./abstractions/vault-filter.service";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
protected _collapsedFilterNodes = new BehaviorSubject<Set<string>>(null);
|
||||
collapsedFilterNodes$: Observable<Set<string>> = this._collapsedFilterNodes.pipe(
|
||||
switchMap(async (nodes) => nodes ?? (await this.getCollapsedFilterNodes()))
|
||||
);
|
||||
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>> =
|
||||
this.organizationService.organizations$.pipe(
|
||||
switchMap((orgs) => this.buildOrganizationTree(orgs))
|
||||
);
|
||||
|
||||
protected _organizationFilter = new BehaviorSubject<Organization>(null);
|
||||
|
||||
filteredFolders$: Observable<FolderView[]> = this.folderService.folderViews$.pipe(
|
||||
combineLatestWith(this._organizationFilter),
|
||||
switchMap(([folders, org]) => {
|
||||
return this.filterFolders(folders, org);
|
||||
})
|
||||
);
|
||||
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
|
||||
map((folders) => this.buildFolderTree(folders))
|
||||
);
|
||||
|
||||
// TODO: Remove once collections is refactored with observables
|
||||
// replace with collection service observable
|
||||
private collectionViews$ = new ReplaySubject<CollectionView[]>(1);
|
||||
filteredCollections$: Observable<CollectionView[]> = this.collectionViews$.pipe(
|
||||
combineLatestWith(this._organizationFilter),
|
||||
switchMap(([collections, org]) => {
|
||||
return this.filterCollections(collections, org);
|
||||
})
|
||||
);
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe(
|
||||
map((collections) => this.buildCollectionTree(collections))
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected stateService: StateService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected folderService: FolderService,
|
||||
protected cipherService: CipherService,
|
||||
protected collectionService: CollectionService,
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService
|
||||
) {}
|
||||
|
||||
// TODO: Remove once collections is refactored with observables
|
||||
async reloadCollections() {
|
||||
this.collectionViews$.next(await this.collectionService.getAllDecrypted());
|
||||
}
|
||||
|
||||
async getCollectionNodeFromTree(id: string) {
|
||||
const collections = await firstValueFrom(this.collectionTree$);
|
||||
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
|
||||
}
|
||||
|
||||
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
|
||||
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
|
||||
this._collapsedFilterNodes.next(collapsedFilterNodes);
|
||||
}
|
||||
|
||||
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
|
||||
const nodes = new Set(await this.stateService.getCollapsedGroupings());
|
||||
return nodes;
|
||||
}
|
||||
|
||||
setOrganizationFilter(organization: Organization) {
|
||||
if (organization?.id != "AllVaults") {
|
||||
this._organizationFilter.next(organization);
|
||||
} else {
|
||||
this._organizationFilter.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
async expandOrgFilter() {
|
||||
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
|
||||
if (!collapsedFilterNodes.has("AllVaults")) {
|
||||
return;
|
||||
}
|
||||
collapsedFilterNodes.delete("AllVaults");
|
||||
await this.setCollapsedFilterNodes(collapsedFilterNodes);
|
||||
}
|
||||
|
||||
protected async buildOrganizationTree(
|
||||
orgs?: Organization[]
|
||||
): Promise<TreeNode<OrganizationFilter>> {
|
||||
const headNode = this.getOrganizationFilterHead();
|
||||
if (!(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))) {
|
||||
const myVaultNode = this.getOrganizationFilterMyVault();
|
||||
headNode.children.push(myVaultNode);
|
||||
}
|
||||
if (await this.policyService.policyAppliesToUser(PolicyType.SingleOrg)) {
|
||||
orgs = orgs.slice(0, 1);
|
||||
}
|
||||
if (orgs) {
|
||||
orgs.forEach((org) => {
|
||||
const orgCopy = org as OrganizationFilter;
|
||||
orgCopy.icon = "bwi-business";
|
||||
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode, orgCopy.name);
|
||||
headNode.children.push(node);
|
||||
});
|
||||
}
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getOrganizationFilterHead(): TreeNode<OrganizationFilter> {
|
||||
const head = new Organization() as OrganizationFilter;
|
||||
head.enabled = true;
|
||||
return new TreeNode<OrganizationFilter>(head, null, "allVaults", "AllVaults");
|
||||
}
|
||||
|
||||
protected getOrganizationFilterMyVault(): TreeNode<OrganizationFilter> {
|
||||
const myVault = new Organization() as OrganizationFilter;
|
||||
myVault.id = "MyVault";
|
||||
myVault.icon = "bwi-user";
|
||||
myVault.enabled = true;
|
||||
myVault.hideOptions = true;
|
||||
return new TreeNode<OrganizationFilter>(myVault, null, this.i18nService.t("myVault"));
|
||||
}
|
||||
|
||||
buildTypeTree(
|
||||
head: CipherTypeFilter,
|
||||
array?: CipherTypeFilter[]
|
||||
): Observable<TreeNode<CipherTypeFilter>> {
|
||||
const headNode = new TreeNode<CipherTypeFilter>(head, null);
|
||||
array?.forEach((filter) => {
|
||||
const node = new TreeNode<CipherTypeFilter>(filter, headNode, filter.name);
|
||||
headNode.children.push(node);
|
||||
});
|
||||
return of(headNode);
|
||||
}
|
||||
|
||||
protected async filterCollections(
|
||||
storedCollections: CollectionView[],
|
||||
org?: Organization
|
||||
): Promise<CollectionView[]> {
|
||||
return org?.id != null
|
||||
? storedCollections.filter((c) => c.organizationId === org.id)
|
||||
: storedCollections;
|
||||
}
|
||||
|
||||
protected buildCollectionTree(collections?: CollectionView[]): TreeNode<CollectionFilter> {
|
||||
const headNode = this.getCollectionFilterHead();
|
||||
if (!collections) {
|
||||
return headNode;
|
||||
}
|
||||
const nodes: TreeNode<CollectionFilter>[] = [];
|
||||
collections
|
||||
.sort((a, b) => this.i18nService.collator.compare(a.name, b.name))
|
||||
.forEach((c) => {
|
||||
const collectionCopy = new CollectionView() as CollectionFilter;
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
collectionCopy.icon = "bwi-collection";
|
||||
if (c instanceof CollectionAdminView) {
|
||||
collectionCopy.groups = c.groups;
|
||||
collectionCopy.assigned = c.assigned;
|
||||
}
|
||||
const parts =
|
||||
c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||
});
|
||||
nodes.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
|
||||
const head = new CollectionView() as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
|
||||
}
|
||||
|
||||
protected async filterFolders(
|
||||
storedFolders: FolderView[],
|
||||
org?: Organization
|
||||
): Promise<FolderView[]> {
|
||||
if (org?.id == null) {
|
||||
return storedFolders;
|
||||
}
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id);
|
||||
return storedFolders.filter(
|
||||
(f) =>
|
||||
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
|
||||
ciphers.filter((c) => c.folderId == f.id).length < 1
|
||||
);
|
||||
}
|
||||
|
||||
protected buildFolderTree(folders?: FolderView[]): TreeNode<FolderFilter> {
|
||||
const headNode = this.getFolderFilterHead();
|
||||
if (!folders) {
|
||||
return headNode;
|
||||
}
|
||||
const nodes: TreeNode<FolderFilter>[] = [];
|
||||
folders.forEach((f) => {
|
||||
const folderCopy = new FolderView() as FolderFilter;
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
folderCopy.icon = "bwi-folder";
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
|
||||
nodes.forEach((n) => {
|
||||
n.parent = headNode;
|
||||
headNode.children.push(n);
|
||||
});
|
||||
return headNode;
|
||||
}
|
||||
|
||||
protected getFolderFilterHead(): TreeNode<FolderFilter> {
|
||||
const head = new FolderView() as FolderFilter;
|
||||
return new TreeNode<FolderFilter>(head, null, "folders", "AllFolders");
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<ng-container *ngIf="filters && filters.length">
|
||||
<div *ngIf="headerInfo.showHeader" class="filter-heading">
|
||||
<button
|
||||
class="toggle-button"
|
||||
(click)="toggleCollapse(headerNode.node)"
|
||||
[attr.aria-expanded]="!isCollapsed(headerNode.node)"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }}: {{ headerNode.node.name | i18n }}"
|
||||
aria-controls="sub-filters"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="isCollapsed(headerNode.node) ? 'bwi-angle-right' : 'bwi-angle-down'"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="headerInfo.isSelectable"
|
||||
appA11yTitle="{{ isOrganizationFilter ? 'vault' : ('filter' | i18n) }}: {{
|
||||
headerNode.node.name | i18n
|
||||
}}"
|
||||
class="filter-button"
|
||||
(click)="onFilterSelect(headerNode)"
|
||||
>
|
||||
<h3
|
||||
[ngClass]="{
|
||||
active: isAllVaultsSelected || isNodeSelected(headerNode)
|
||||
}"
|
||||
>
|
||||
{{ headerNode.node.name | i18n }}
|
||||
</h3>
|
||||
</button>
|
||||
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
|
||||
{{ headerNode.node.name | i18n }}
|
||||
</h3>
|
||||
|
||||
<button
|
||||
*ngIf="showAddButton"
|
||||
(click)="onAdd()"
|
||||
class="text-muted ml-auto add-button"
|
||||
appA11yTitle="{{ addInfo.text | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
id="{{ headerNode.node.name }}-filters"
|
||||
*ngIf="!isCollapsed(headerNode.node)"
|
||||
class="filter-options"
|
||||
>
|
||||
<ng-template #recursiveFilters let-filters>
|
||||
<li
|
||||
*ngFor="let f of filters"
|
||||
[ngClass]="{
|
||||
active: isNodeSelected(f)
|
||||
}"
|
||||
class="filter-option"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
*ngIf="f.children.length"
|
||||
appA11yTitle="{{ 'toggleCollapse' | i18n }}: {{ f.node.name }}"
|
||||
(click)="toggleCollapse(f.node)"
|
||||
[attr.aria-expanded]="!isCollapsed(f.node)"
|
||||
[attr.aria-controls]="f.node.name + '_children'"
|
||||
class="toggle-button"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
[ngClass]="{
|
||||
'bwi-angle-right': isCollapsed(f.node),
|
||||
'bwi-angle-down': !isCollapsed(f.node)
|
||||
}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
class="filter-button"
|
||||
appA11yTitle="{{ isOrganizationFilter ? 'vault' : ('filter' | i18n) }}: {{
|
||||
f.node.name
|
||||
}}"
|
||||
[ngClass]="{ 'disabled-organization': isOrganizationFilter && !f.node.enabled }"
|
||||
(click)="onFilterSelect(f)"
|
||||
>
|
||||
<i
|
||||
*ngIf="f.children.length === 0"
|
||||
class="bwi bwi-fw {{ f.node.icon }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{ f.node.name }}
|
||||
</button>
|
||||
<span class="ml-auto">
|
||||
<button
|
||||
*ngIf="editInfo && f.node.id"
|
||||
class="edit-button"
|
||||
(click)="onEdit(f)"
|
||||
appA11yTitle="{{ editInfo.text | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<i
|
||||
*ngIf="isOrganizationFilter && !f.node.enabled"
|
||||
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
|
||||
[attr.aria-label]="'organizationIsDisabled' | i18n"
|
||||
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
||||
></i
|
||||
><ng-container *ngIf="optionsInfo && !f.node.hideOptions"
|
||||
><button [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon">
|
||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu class="filter-organization-options" #optionsMenu>
|
||||
<ng-container
|
||||
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||
></ng-container>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<ul
|
||||
[id]="f.node.name + '_children'"
|
||||
class="nested-filter-options"
|
||||
*ngIf="f.children.length && !isCollapsed(f.node)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="recursiveFilters; context: { $implicit: f.children }">
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-template>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="recursiveFilters; context: { $implicit: filters }"
|
||||
></ng-container>
|
||||
<li class="filter-option" *ngIf="showAddLink">
|
||||
<span class="filter-buttons">
|
||||
<a href="#" routerLink="{{ addInfo.route }}" class="filter-button">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ addInfo.text | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<hr *ngIf="divider" />
|
||||
</ng-container>
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService } from "../../services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type";
|
||||
import { VaultFilter } from "../models/vault-filter.model";
|
||||
|
||||
@Component({
|
||||
selector: "app-filter-section",
|
||||
templateUrl: "vault-filter-section.component.html",
|
||||
})
|
||||
export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Input() section: VaultFilterSection;
|
||||
|
||||
data: TreeNode<VaultFilterType>;
|
||||
collapsedFilterNodes: Set<string> = new Set();
|
||||
|
||||
private injectors = new Map<string, Injector>();
|
||||
|
||||
constructor(private vaultFilterService: VaultFilterService, private injector: Injector) {
|
||||
this.vaultFilterService.collapsedFilterNodes$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((nodes) => {
|
||||
this.collapsedFilterNodes = nodes;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
this.data = data;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
get headerNode() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
get headerInfo() {
|
||||
return this.section.header;
|
||||
}
|
||||
|
||||
get filters() {
|
||||
return this.data?.children;
|
||||
}
|
||||
|
||||
get isOrganizationFilter() {
|
||||
return this.data.node instanceof Organization;
|
||||
}
|
||||
|
||||
get isAllVaultsSelected() {
|
||||
return this.isOrganizationFilter && !this.activeFilter.selectedOrganizationNode;
|
||||
}
|
||||
|
||||
isNodeSelected(filterNode: TreeNode<VaultFilterType>) {
|
||||
return (
|
||||
this.activeFilter.organizationId === filterNode?.node.id ||
|
||||
this.activeFilter.cipherTypeId === filterNode?.node.id ||
|
||||
this.activeFilter.folderId === filterNode?.node.id ||
|
||||
this.activeFilter.collectionId === filterNode?.node.id
|
||||
);
|
||||
}
|
||||
|
||||
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
|
||||
await this.section?.action(filterNode);
|
||||
}
|
||||
|
||||
get editInfo() {
|
||||
return this.section?.edit;
|
||||
}
|
||||
|
||||
onEdit(filterNode: TreeNode<VaultFilterType>) {
|
||||
this.section?.edit?.action(filterNode.node);
|
||||
}
|
||||
|
||||
get addInfo() {
|
||||
return this.section.add;
|
||||
}
|
||||
|
||||
get showAddButton() {
|
||||
return this.section.add && !this.section.add.route;
|
||||
}
|
||||
|
||||
get showAddLink() {
|
||||
return this.section.add && this.section.add.route;
|
||||
}
|
||||
|
||||
async onAdd() {
|
||||
this.section?.add?.action();
|
||||
}
|
||||
|
||||
get optionsInfo() {
|
||||
return this.section?.options;
|
||||
}
|
||||
|
||||
get divider() {
|
||||
return this.section?.divider;
|
||||
}
|
||||
|
||||
isCollapsed(node: ITreeNodeObject) {
|
||||
return this.collapsedFilterNodes.has(node.id);
|
||||
}
|
||||
|
||||
async toggleCollapse(node: ITreeNodeObject) {
|
||||
if (this.collapsedFilterNodes.has(node.id)) {
|
||||
this.collapsedFilterNodes.delete(node.id);
|
||||
} else {
|
||||
this.collapsedFilterNodes.add(node.id);
|
||||
}
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes);
|
||||
}
|
||||
|
||||
// an injector is necessary to pass data into a dynamic component
|
||||
// here we are creating a new injector for each filter that has options
|
||||
createInjector(data: VaultFilterType) {
|
||||
let inject = this.injectors.get(data.id);
|
||||
if (!inject) {
|
||||
inject = Injector.create({
|
||||
providers: [{ provide: OptionsInput, useValue: data }],
|
||||
parent: this.injector,
|
||||
});
|
||||
this.injectors.set(data.id, inject);
|
||||
}
|
||||
return inject;
|
||||
}
|
||||
}
|
||||
export const OptionsInput = new InjectionToken<VaultFilterType>("OptionsInput");
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { TreeNode } from "@bitwarden/common/src/models/domain/tree-node";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
export type VaultFilterType =
|
||||
| OrganizationFilter
|
||||
| CipherTypeFilter
|
||||
| FolderFilter
|
||||
| CollectionFilter;
|
||||
|
||||
export enum VaultFilterLabel {
|
||||
OrganizationFilter = "organizationFilter",
|
||||
TypeFilter = "typeFilter",
|
||||
FolderFilter = "folderFilter",
|
||||
CollectionFilter = "collectionFilter",
|
||||
TrashFilter = "trashFilter",
|
||||
}
|
||||
|
||||
export type VaultFilterSection = {
|
||||
data$: Observable<TreeNode<VaultFilterType>>;
|
||||
header: {
|
||||
showHeader: boolean;
|
||||
isSelectable: boolean;
|
||||
};
|
||||
action: (filterNode: TreeNode<VaultFilterType>) => Promise<void>;
|
||||
edit?: {
|
||||
text: string;
|
||||
action: (filter: VaultFilterType) => void;
|
||||
};
|
||||
add?: {
|
||||
text: string;
|
||||
route?: string;
|
||||
action?: () => void;
|
||||
};
|
||||
options?: {
|
||||
component: any;
|
||||
};
|
||||
divider?: boolean;
|
||||
};
|
||||
|
||||
export type VaultFilterList = {
|
||||
[key in VaultFilterLabel]?: VaultFilterSection;
|
||||
};
|
||||
@@ -1,332 +0,0 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
|
||||
import { VaultFilter } from "./vault-filter.model";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
describe("VaultFilter", () => {
|
||||
describe("filterFunction", () => {
|
||||
const allCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "AllItems",
|
||||
name: "allItems",
|
||||
type: "all",
|
||||
icon: "",
|
||||
},
|
||||
null
|
||||
);
|
||||
const favoriteCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "favorites",
|
||||
name: "favorites",
|
||||
type: "favorites",
|
||||
icon: "bwi-star",
|
||||
},
|
||||
null
|
||||
);
|
||||
const identityCiphersFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "identity",
|
||||
name: "identity",
|
||||
type: CipherType.Identity,
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
null
|
||||
);
|
||||
const trashFilter = new TreeNode<CipherTypeFilter>(
|
||||
{
|
||||
id: "trash",
|
||||
name: "trash",
|
||||
type: "trash",
|
||||
icon: "bwi-trash",
|
||||
},
|
||||
null
|
||||
);
|
||||
describe("generic cipher", () => {
|
||||
it("should return true when no filter is applied", () => {
|
||||
const cipher = createCipher();
|
||||
const filterFunction = createFilterFunction({});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a favorite cipher", () => {
|
||||
const cipher = createCipher({ favorite: true });
|
||||
|
||||
it("should return true when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: allCiphersFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a deleted cipher", () => {
|
||||
const cipher = createCipher({ deletedDate: new Date() });
|
||||
|
||||
it("should return true when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: favoriteCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with type", () => {
|
||||
it("should return true when filter matches cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Identity });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: identityCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Card });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCipherTypeNode: identityCiphersFilter,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with folder id", () => {
|
||||
it("should return true when filter matches folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: "folderId" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: "differentFolderId" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher without folder", () => {
|
||||
const cipher = createCipher({ folderId: null });
|
||||
|
||||
it("should return true when filtering on unassigned folder", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolderNode: createFolderFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an organizational cipher (with organization and collections)", () => {
|
||||
const cipher = createCipher({
|
||||
organizationId: "organizationId",
|
||||
collectionIds: ["collectionId", "anotherId"],
|
||||
});
|
||||
|
||||
it("should return true when filter matches collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "collectionId",
|
||||
organizationId: "organizationId",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "nonMatchingCollectionId",
|
||||
organizationId: "organizationId",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({
|
||||
id: "nonMatchingOrganizationId",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering by All Collections", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: "AllCollections" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
|
||||
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
|
||||
|
||||
it("should return true when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({ id: "organizationId" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an individual cipher (without organization or collection)", () => {
|
||||
const cipher = createCipher({ organizationId: null, collectionIds: [] });
|
||||
|
||||
it("should return false when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: null }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when filtering for my vault only", () => {
|
||||
const cipher = createCipher({ organizationId: null });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterFunction(options: Partial<VaultFilter> = {}) {
|
||||
return new VaultFilter(options).buildFilter();
|
||||
}
|
||||
|
||||
function createOrganizationFilterNode(
|
||||
options: Partial<OrganizationFilter>
|
||||
): TreeNode<OrganizationFilter> {
|
||||
const org = new Organization() as OrganizationFilter;
|
||||
org.id = options.id;
|
||||
org.icon = options.icon ?? "";
|
||||
return new TreeNode<OrganizationFilter>(org, null);
|
||||
}
|
||||
|
||||
function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<FolderFilter> {
|
||||
const folder = new FolderView() as FolderFilter;
|
||||
folder.id = options.id;
|
||||
folder.name = options.name;
|
||||
folder.icon = options.icon ?? "";
|
||||
folder.revisionDate = options.revisionDate ?? new Date();
|
||||
return new TreeNode<FolderFilter>(folder, null);
|
||||
}
|
||||
|
||||
function createCollectionFilterNode(
|
||||
options: Partial<CollectionFilter>
|
||||
): TreeNode<CollectionFilter> {
|
||||
const collection = new CollectionView() as CollectionFilter;
|
||||
collection.id = options.id;
|
||||
collection.name = options.name ?? "";
|
||||
collection.icon = options.icon ?? "";
|
||||
collection.organizationId = options.organizationId;
|
||||
collection.externalId = options.externalId ?? "";
|
||||
collection.readOnly = options.readOnly ?? false;
|
||||
collection.hidePasswords = options.hidePasswords ?? false;
|
||||
return new TreeNode<CollectionFilter>(collection, null);
|
||||
}
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
const cipher = new CipherView();
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
return cipher;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
CipherStatus,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter.type";
|
||||
|
||||
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||
|
||||
// TODO: Replace shared VaultFilter Model with this one and
|
||||
// refactor browser and desktop code to use this model.
|
||||
export class VaultFilter {
|
||||
selectedOrganizationNode: TreeNode<OrganizationFilter>;
|
||||
selectedCipherTypeNode: TreeNode<CipherTypeFilter>;
|
||||
selectedFolderNode: TreeNode<FolderFilter>;
|
||||
selectedCollectionNode: TreeNode<CollectionFilter>;
|
||||
|
||||
get isFavorites(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "favorites";
|
||||
}
|
||||
|
||||
get isDeleted(): boolean {
|
||||
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
||||
}
|
||||
|
||||
get organizationId(): string {
|
||||
return this.selectedOrganizationNode?.node.id;
|
||||
}
|
||||
|
||||
get cipherType(): CipherType {
|
||||
return this.selectedCipherTypeNode?.node.type in CipherType
|
||||
? (this.selectedCipherTypeNode?.node.type as CipherType)
|
||||
: null;
|
||||
}
|
||||
|
||||
get cipherStatus(): CipherStatus {
|
||||
return this.selectedCipherTypeNode?.node.type;
|
||||
}
|
||||
|
||||
get cipherTypeId(): string {
|
||||
return this.selectedCipherTypeNode?.node.id;
|
||||
}
|
||||
|
||||
get folderId(): string {
|
||||
return this.selectedFolderNode?.node.id;
|
||||
}
|
||||
|
||||
get collectionId(): string {
|
||||
return this.selectedCollectionNode?.node.id;
|
||||
}
|
||||
|
||||
constructor(init?: Partial<VaultFilter>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.selectedCipherTypeNode = null;
|
||||
this.selectedFolderNode = null;
|
||||
this.selectedCollectionNode = null;
|
||||
}
|
||||
|
||||
resetOrganization() {
|
||||
this.selectedOrganizationNode = null;
|
||||
}
|
||||
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.isFavorites && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.isDeleted && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.cipherType && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.cipherType;
|
||||
}
|
||||
if (this.selectedFolderNode) {
|
||||
// No folder
|
||||
if (this.folderId === null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === null;
|
||||
}
|
||||
// Folder
|
||||
if (this.folderId !== null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.folderId;
|
||||
}
|
||||
}
|
||||
if (this.selectedCollectionNode) {
|
||||
// All Collections
|
||||
if (this.collectionId === "AllCollections" && cipherPassesFilter) {
|
||||
cipherPassesFilter = false;
|
||||
}
|
||||
// Unassigned
|
||||
if (this.collectionId === null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.organizationId != null &&
|
||||
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
|
||||
}
|
||||
// Collection
|
||||
if (
|
||||
this.collectionId !== null &&
|
||||
this.collectionId !== "AllCollections" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.includes(this.collectionId);
|
||||
}
|
||||
}
|
||||
if (this.selectedOrganizationNode) {
|
||||
// My Vault
|
||||
if (this.organizationId === "MyVault" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
// Organization
|
||||
else if (this.organizationId !== null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.organizationId;
|
||||
}
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { CipherType } from "@bitwarden/common/src/enums/cipherType";
|
||||
import { Organization } from "@bitwarden/common/src/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/src/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/src/models/view/folder.view";
|
||||
|
||||
import { CollectionAdminView } from "../../../../organizations/core";
|
||||
|
||||
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
|
||||
|
||||
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||
export type CollectionFilter = CollectionAdminView & {
|
||||
icon: string;
|
||||
};
|
||||
export type FolderFilter = FolderView & { icon: string };
|
||||
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [VaultFilterSectionComponent],
|
||||
exports: [SharedModule, VaultFilterSectionComponent],
|
||||
})
|
||||
export class VaultFilterSharedModule {}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LinkSsoComponent } from "./components/link-sso.component";
|
||||
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||
import { VaultFilterComponent } from "./components/vault-filter.component";
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
|
||||
import { VaultFilterService } from "./services/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "./shared/vault-filter-shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
@@ -1,341 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<ng-container>
|
||||
<bit-table
|
||||
*ngIf="filteredCiphers.length || filteredCollections.length"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-min-w-fit" colspan="2">
|
||||
<input
|
||||
class="tw-mr-2"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
id="checkAll"
|
||||
(change)="checkAll($any($event.target).checked)"
|
||||
[(ngModel)]="isAllChecked"
|
||||
/>
|
||||
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="checkAll">{{
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
<th bitCell class="tw-w-1/2">{{ "name" | i18n }}</th>
|
||||
<th bitCell class="tw-w-max">
|
||||
<ng-container *ngIf="!organization">{{ "owner" | i18n }}</ng-container>
|
||||
<ng-container *ngIf="organization">
|
||||
{{ (activeFilter.selectedCollectionNode ? "groups" : "collections") | i18n }}
|
||||
</ng-container>
|
||||
</th>
|
||||
<th bitCell class="tw-min-w-fit tw-text-right">
|
||||
<button
|
||||
[bitMenuTriggerFor]="headerMenu"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #headerMenu>
|
||||
<ng-container>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
appStopClick
|
||||
(click)="bulkMove()"
|
||||
*ngIf="!activeFilter.isDeleted && !organization"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ "moveSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
appStopClick
|
||||
(click)="bulkShare()"
|
||||
*ngIf="!activeFilter.isDeleted && !organization"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
|
||||
{{ "moveSelectedToOrg" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="activeFilter.isDeleted">
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restoreSelected" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" (click)="bulkDelete()">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{
|
||||
(activeFilter.isDeleted ? "permanentlyDeleteSelected" : "deleteSelected") | i18n
|
||||
}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr
|
||||
bitRow
|
||||
*ngFor="let col of filteredCollections"
|
||||
(click)="navigateCollection(col)"
|
||||
class="tw-cursor-pointer"
|
||||
alignContent="middle"
|
||||
>
|
||||
<td bitCell (click)="checkRow(col)" appStopProp>
|
||||
<input
|
||||
*ngIf="canDeleteCollection(col.node)"
|
||||
class="tw-cursor-pointer"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
[(ngModel)]="$any(col).checked"
|
||||
appStopProp
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<div class="icon" aria-hidden="true">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-collection"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
bitLink
|
||||
class="tw-text-start"
|
||||
linkType="secondary"
|
||||
(click)="navigateCollection(col)"
|
||||
>
|
||||
{{ col.node.name }}
|
||||
</button>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization">
|
||||
<app-org-badge
|
||||
organizationName="{{ col.node.organizationId | orgNameFromId: organizations }}"
|
||||
[profileName]="profileName"
|
||||
(onOrganizationClicked)="onOrganizationClicked(col.node.organizationId)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="organization && activeFilter.selectedCollectionNode">
|
||||
<app-group-badge
|
||||
*ngIf="col.node.groups"
|
||||
[selectedGroups]="col.node.groups"
|
||||
[allGroups]="groups"
|
||||
></app-group-badge>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<button
|
||||
*ngIf="canEditCollection(col.node) || canDeleteCollection(col.node)"
|
||||
[bitMenuTriggerFor]="collectionOptions"
|
||||
size="small"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
appStopProp
|
||||
></button>
|
||||
<bit-menu #collectionOptions>
|
||||
<button
|
||||
*ngIf="canEditCollection(col.node)"
|
||||
bitMenuItem
|
||||
(click)="editCollection(col.node, 'info')"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "editInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="canEditCollection(col.node)"
|
||||
bitMenuItem
|
||||
(click)="editCollection(col.node, 'access')"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="canDeleteCollection(col.node)"
|
||||
bitMenuItem
|
||||
(click)="deleteCollection(col.node)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
bitRow
|
||||
*ngFor="let c of filteredCiphers"
|
||||
class="tw-cursor-pointer"
|
||||
(click)="selectCipher(c)"
|
||||
alignContent="middle"
|
||||
>
|
||||
<td bitCell (click)="checkRow(c)" appStopProp>
|
||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(c).checked" appStopProp />
|
||||
</td>
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell class="tw-break-all">
|
||||
<button
|
||||
bitLink
|
||||
class="tw-text-start"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ itemId: c.id }"
|
||||
queryParamsHandling="merge"
|
||||
title="{{ 'editItem' | i18n }}"
|
||||
>
|
||||
{{ c.name }}
|
||||
</button>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
<ng-container *ngIf="showFixOldAttachments(c)">
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle text-warning"
|
||||
appStopProp
|
||||
title="{{ 'attachmentsNeedFix' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachmentsNeedFix" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<br />
|
||||
<span class="tw-text-sm tw-text-muted" appStopProp>{{ c.subTitle }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization">
|
||||
<app-org-badge
|
||||
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
|
||||
profileName="{{ profileName }}"
|
||||
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="organization && !activeFilter.selectedCollectionNode">
|
||||
<app-collection-badge
|
||||
*ngIf="c.collectionIds"
|
||||
[collectionIds]="c.collectionIds"
|
||||
[collections]="vaultFilterService.filteredCollections$ | async"
|
||||
></app-collection-badge>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<button
|
||||
[bitMenuTriggerFor]="cipherOptions"
|
||||
size="small"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
appStopProp
|
||||
></button>
|
||||
<bit-menu #cipherOptions>
|
||||
<ng-container *ngIf="c.type === cipherType.Login && !c.isDeleted">
|
||||
<button bitMenuItem (click)="copy(c, c.login.username, 'username', 'Username')">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
(click)="copy(c, c.login.password, 'password', 'Password')"
|
||||
*ngIf="c.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
(click)="copy(c, c.login.totp, 'verificationCodeTotp', 'TOTP')"
|
||||
*ngIf="displayTotpCopyButton(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyVerificationCode" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="c.login.canLaunch" (click)="launch(c.login.launchUri)">
|
||||
<i class="bwi bwi-fw bwi-share-square" aria-hidden="true"></i>
|
||||
{{ "launch" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button bitMenuItem (click)="attachments(c)">
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
|
||||
(click)="clone(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
|
||||
{{ "clone" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="!organization && !c.organizationId && !c.isDeleted"
|
||||
(click)="share(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
|
||||
{{ "moveToOrganization" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="c.organizationId && !c.isDeleted"
|
||||
(click)="editCipherCollections(c)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="c.organizationId && accessEvents" (click)="events(c)">
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="restore(c)" *ngIf="c.isDeleted">
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="deleteCipher(c)">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (c.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="
|
||||
showMissingCollectionPermissionMessage ||
|
||||
(!filteredCiphers.length && !filteredCollections.length)
|
||||
"
|
||||
>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
|
||||
<ng-container *ngIf="showMissingCollectionPermissionMessage">
|
||||
<p>{{ "noPermissionToViewAllCollectionItems" | i18n }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!showMissingCollectionPermissionMessage">
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button (click)="addCipher()" class="btn btn-outline-primary" *ngIf="showAddNew">
|
||||
<i class="bwi bwi-plus bwi-fw"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,569 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/components/vault-items.component";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { EventType } from "@bitwarden/common/enums/eventType";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||
import { DialogService, Icons } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAdminView, GroupView } from "../organizations/core";
|
||||
|
||||
import {
|
||||
BulkDeleteDialogResult,
|
||||
openBulkDeleteDialog,
|
||||
} from "./bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import {
|
||||
BulkMoveDialogResult,
|
||||
openBulkMoveDialog,
|
||||
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
|
||||
import {
|
||||
BulkRestoreDialogResult,
|
||||
openBulkRestoreDialog,
|
||||
} from "./bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component";
|
||||
import {
|
||||
BulkShareDialogResult,
|
||||
openBulkShareDialog,
|
||||
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { CollectionFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
const MaxCheckedCount = 500;
|
||||
|
||||
export type VaultItemRow = (CipherView | TreeNode<CollectionFilter>) & { checked?: boolean };
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
})
|
||||
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy {
|
||||
@Input() showAddNew = true;
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
|
||||
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
|
||||
@Output() onShareClicked = new EventEmitter<CipherView>();
|
||||
@Output() onEditCipherCollectionsClicked = new EventEmitter<CipherView>();
|
||||
@Output() onCloneClicked = new EventEmitter<CipherView>();
|
||||
@Output() onOrganzationBadgeClicked = new EventEmitter<string>();
|
||||
|
||||
cipherType = CipherType;
|
||||
actionPromise: Promise<any>;
|
||||
userHasPremiumAccess = false;
|
||||
organizations: Organization[] = [];
|
||||
profileName: string;
|
||||
noItemIcon = Icons.Search;
|
||||
groups: GroupView[] = [];
|
||||
|
||||
protected pageSizeLimit = 200;
|
||||
protected isAllChecked = false;
|
||||
protected didScroll = false;
|
||||
protected currentPagedCiphersCount = 0;
|
||||
protected currentPagedCollectionsCount = 0;
|
||||
protected refreshing = false;
|
||||
|
||||
protected pagedCiphers: CipherView[] = [];
|
||||
protected pagedCollections: TreeNode<CollectionFilter>[] = [];
|
||||
protected searchedCollections: TreeNode<CollectionFilter>[] = [];
|
||||
|
||||
get collections(): TreeNode<CollectionFilter>[] {
|
||||
return this.activeFilter?.selectedCollectionNode?.children ?? [];
|
||||
}
|
||||
|
||||
get filteredCollections(): TreeNode<CollectionFilter>[] {
|
||||
if (this.isPaging()) {
|
||||
return this.pagedCollections;
|
||||
}
|
||||
|
||||
if (this.searchService.isSearchable(this.searchText)) {
|
||||
return this.searchedCollections;
|
||||
}
|
||||
|
||||
return this.collections;
|
||||
}
|
||||
|
||||
get filteredCiphers(): CipherView[] {
|
||||
return this.isPaging() ? this.pagedCiphers : this.ciphers;
|
||||
}
|
||||
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected vaultFilterService: VaultFilterService,
|
||||
protected cipherService: CipherService,
|
||||
protected eventCollectionService: EventCollectionService,
|
||||
protected totpService: TotpService,
|
||||
protected stateService: StateService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
protected dialogService: DialogService,
|
||||
protected logService: LogService,
|
||||
private searchPipe: SearchPipe,
|
||||
private organizationService: OrganizationService,
|
||||
private tokenService: TokenService
|
||||
) {
|
||||
super(searchService);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.checkAll(false);
|
||||
}
|
||||
|
||||
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||
this.checkAll(false);
|
||||
this.isAllChecked = false;
|
||||
this.pagedCollections = [];
|
||||
if (!this.refreshing && this.isPaging()) {
|
||||
this.currentPagedCollectionsCount = 0;
|
||||
this.currentPagedCiphersCount = 0;
|
||||
}
|
||||
await super.applyFilter(filter);
|
||||
}
|
||||
|
||||
// load() is called after the page loads and the first sync has completed.
|
||||
// Do not use ngOnInit() for anything that requires sync data.
|
||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||
await super.load(filter, deleted);
|
||||
this.updateSearchedCollections(this.collections);
|
||||
this.profileName = await this.tokenService.getName();
|
||||
this.organizations = await this.organizationService.getAll();
|
||||
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.refreshing = true;
|
||||
await this.reload(this.filter, this.deleted);
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
// If we have less rows than the page size, we don't need to page anything
|
||||
if (this.ciphers.length + (this.collections?.length || 0) <= this.pageSizeLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pageSpaceLeft = this.pageSizeLimit;
|
||||
if (
|
||||
this.refreshing &&
|
||||
this.pagedCiphers.length + this.pagedCollections.length === 0 &&
|
||||
this.currentPagedCiphersCount + this.currentPagedCollectionsCount > this.pageSizeLimit
|
||||
) {
|
||||
// When we refresh, we want to load the previous amount of items, not restart the paging
|
||||
pageSpaceLeft = this.currentPagedCiphersCount + this.currentPagedCollectionsCount;
|
||||
}
|
||||
// if there are still collections to show
|
||||
if (this.collections?.length > this.pagedCollections.length) {
|
||||
const collectionsToAdd = this.collections.slice(
|
||||
this.pagedCollections.length,
|
||||
this.currentPagedCollectionsCount + pageSpaceLeft
|
||||
);
|
||||
this.pagedCollections = this.pagedCollections.concat(collectionsToAdd);
|
||||
// set the current count to the new count of paged collections
|
||||
this.currentPagedCollectionsCount = this.pagedCollections.length;
|
||||
// subtract the available page size by the amount of collections we just added, default to 0 if negative
|
||||
pageSpaceLeft =
|
||||
collectionsToAdd.length > pageSpaceLeft ? 0 : pageSpaceLeft - collectionsToAdd.length;
|
||||
}
|
||||
// if we have room left to show ciphers and we have ciphers to show
|
||||
if (pageSpaceLeft > 0 && this.ciphers.length > this.pagedCiphers.length) {
|
||||
this.pagedCiphers = this.pagedCiphers.concat(
|
||||
this.ciphers.slice(this.pagedCiphers.length, this.currentPagedCiphersCount + pageSpaceLeft)
|
||||
);
|
||||
// set the current count to the new count of paged ciphers
|
||||
this.currentPagedCiphersCount = this.pagedCiphers.length;
|
||||
}
|
||||
// set a flag if we actually loaded the second page while paging
|
||||
this.didScroll = this.pagedCiphers.length + this.pagedCollections.length > this.pageSizeLimit;
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
const totalRows =
|
||||
this.ciphers.length + (this.activeFilter?.selectedCollectionNode?.children.length || 0);
|
||||
return !searching && totalRows > this.pageSizeLimit;
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedCollections = [];
|
||||
this.pagedCiphers = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
async doSearch(indexedCiphers?: CipherView[]) {
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
[this.filter, this.deletedFilter],
|
||||
indexedCiphers
|
||||
);
|
||||
this.updateSearchedCollections(this.collections);
|
||||
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);
|
||||
}
|
||||
|
||||
editCipherCollections(c: CipherView) {
|
||||
this.onEditCipherCollectionsClicked.emit(c);
|
||||
}
|
||||
|
||||
async clone(c: CipherView) {
|
||||
if (!(await this.repromptCipher(c))) {
|
||||
return;
|
||||
}
|
||||
this.onCloneClicked.emit(c);
|
||||
}
|
||||
|
||||
async deleteCipher(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.deleteCipherWithServer(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 bulkDelete() {
|
||||
if (!(await this.repromptCipher())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = this.selectedCipherIds;
|
||||
if (selectedIds.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
||||
data: { permanent: this.deleted, cipherIds: selectedIds },
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkDeleteDialogResult.Deleted) {
|
||||
this.actionPromise = this.refresh();
|
||||
await this.actionPromise;
|
||||
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 bulkRestore() {
|
||||
if (!(await this.repromptCipher())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCipherIds = this.selectedCipherIds;
|
||||
if (selectedCipherIds.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = openBulkRestoreDialog(this.dialogService, {
|
||||
data: { cipherIds: selectedCipherIds },
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkRestoreDialogResult.Restored) {
|
||||
this.actionPromise = this.refresh();
|
||||
await this.actionPromise;
|
||||
this.actionPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async bulkShare() {
|
||||
if (!(await this.repromptCipher())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCiphers = this.selectedCiphers;
|
||||
if (selectedCiphers.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers: selectedCiphers } });
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkShareDialogResult.Shared) {
|
||||
this.actionPromise = this.refresh();
|
||||
await this.actionPromise;
|
||||
this.actionPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async bulkMove() {
|
||||
if (!(await this.repromptCipher())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCipherIds = this.selectedCipherIds;
|
||||
if (selectedCipherIds.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("nothingSelected")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = openBulkMoveDialog(this.dialogService, {
|
||||
data: { cipherIds: selectedCipherIds },
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === BulkMoveDialogResult.Moved) {
|
||||
this.actionPromise = this.refresh();
|
||||
await this.actionPromise;
|
||||
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.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||
cipher.id
|
||||
);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
|
||||
}
|
||||
}
|
||||
|
||||
navigateCollection(node: TreeNode<CollectionFilter>) {
|
||||
const filter = this.activeFilter;
|
||||
filter.selectedCollectionNode = node;
|
||||
this.activeFilterChanged.emit(filter);
|
||||
}
|
||||
|
||||
checkAll(select: boolean) {
|
||||
if (select) {
|
||||
this.checkAll(false);
|
||||
}
|
||||
const items: VaultItemRow[] = this.ciphers;
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectCount = select && items.length > MaxCheckedCount ? MaxCheckedCount : items.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkRow(items[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
checkRow(item: VaultItemRow, select?: boolean) {
|
||||
// Collections can't be managed in end user vault
|
||||
if (!(item instanceof CipherView)) {
|
||||
return;
|
||||
}
|
||||
item.checked = select ?? !item.checked;
|
||||
}
|
||||
|
||||
get selectedCiphers(): CipherView[] {
|
||||
if (!this.ciphers) {
|
||||
return [];
|
||||
}
|
||||
return this.ciphers.filter((c) => !!(c as VaultItemRow).checked);
|
||||
}
|
||||
|
||||
get selectedCipherIds(): string[] {
|
||||
return this.selectedCiphers.map((c) => c.id);
|
||||
}
|
||||
|
||||
displayTotpCopyButton(cipher: CipherView) {
|
||||
return (
|
||||
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
|
||||
);
|
||||
}
|
||||
|
||||
onOrganizationClicked(organizationId: string) {
|
||||
this.onOrganzationBadgeClicked.emit(organizationId);
|
||||
}
|
||||
|
||||
events(c: CipherView) {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
}
|
||||
|
||||
canDeleteCollection(c: CollectionAdminView): boolean {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
return false; // Always return false for non org vault
|
||||
}
|
||||
|
||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
}
|
||||
|
||||
canEditCollection(c: CollectionAdminView): boolean {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
return false; // Always return false for non org vault
|
||||
}
|
||||
|
||||
async editCollection(c: CollectionView, tab: "info" | "access"): Promise<void> {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
}
|
||||
|
||||
get showMissingCollectionPermissionMessage(): boolean {
|
||||
// TODO: This should be removed but is needed since we reuse the same template
|
||||
return false; // Always return false for non org vault
|
||||
}
|
||||
|
||||
protected updateSearchedCollections(collections: TreeNode<CollectionFilter>[]) {
|
||||
if (this.searchService.isSearchable(this.searchText)) {
|
||||
this.searchedCollections = this.searchPipe.transform(
|
||||
collections,
|
||||
this.searchText,
|
||||
(collection) => collection.node.name,
|
||||
(collection) => collection.node.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected deleteCipherWithServer(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) {
|
||||
if (c) {
|
||||
return (
|
||||
c.reprompt === CipherRepromptType.None ||
|
||||
(await this.passwordRepromptService.showPasswordPrompt())
|
||||
);
|
||||
} else {
|
||||
const selectedCiphers = this.selectedCiphers;
|
||||
const notProtected = !selectedCiphers.find(
|
||||
(cipher) => cipher.reprompt !== CipherRepromptType.None
|
||||
);
|
||||
|
||||
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { VaultComponent } from "./vault.component";
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: VaultComponent,
|
||||
data: { titleId: "vaults" },
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class VaultRoutingModule {}
|
||||
@@ -1,134 +0,0 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-vault-filter
|
||||
#vaultFilter
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event)"
|
||||
></app-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<bit-breadcrumbs *ngIf="breadcrumbs.length > 0">
|
||||
<bit-breadcrumb
|
||||
*ngFor="let collection of breadcrumbs; let first = first"
|
||||
[icon]="first ? undefined : 'bwi-collection'"
|
||||
(click)="applyCollectionFilter(collection)"
|
||||
>
|
||||
<!-- First node in the tree contains a translation key. The rest come from user input. -->
|
||||
<ng-container *ngIf="first">{{ collection.node.name | i18n }}</ng-container>
|
||||
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
|
||||
</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
<div class="tw-mb-4 tw-flex">
|
||||
<h1>
|
||||
{{ "vaultItems" | i18n }}
|
||||
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
|
||||
<ng-container *ngIf="$any(actionSpinner).loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-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">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="addCipher()"
|
||||
*ngIf="!activeFilter.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-vault-items
|
||||
[activeFilter]="activeFilter"
|
||||
(activeFilterChanged)="applyVaultFilter($event)"
|
||||
(onCipherClicked)="navigateToCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)"
|
||||
(onAddCipher)="addCipher()"
|
||||
(onShareClicked)="shareCipher($event)"
|
||||
(onEditCipherCollectionsClicked)="editCipherCollections($event)"
|
||||
(onCloneClicked)="cloneCipher($event)"
|
||||
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
|
||||
>
|
||||
</app-vault-items>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-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-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
|
||||
|
||||
<app-verify-email
|
||||
*ngIf="showVerifyEmail"
|
||||
class="d-block mb-4"
|
||||
(onVerified)="emailVerified($event)"
|
||||
></app-verify-email>
|
||||
|
||||
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-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="bwi bwi-star-f bwi-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/subscription/premium"
|
||||
>
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #share></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #updateKeyTemplate></ng-template>
|
||||
@@ -1,443 +0,0 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { KdfType, DEFAULT_PBKDF2_ITERATIONS } from "@bitwarden/common/enums/kdfType";
|
||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
|
||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
import { CollectionsComponent } from "./collections.component";
|
||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import {
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
OrganizationFilter,
|
||||
} from "./vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "vault.component.html",
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||
@ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent;
|
||||
@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;
|
||||
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
showPremiumCallout = false;
|
||||
showLowKdf = false;
|
||||
trashCleanupWarning: string = null;
|
||||
kdfIterations: number;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
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 vaultFilterService: VaultFilterService,
|
||||
private cipherService: CipherService,
|
||||
private passwordRepromptService: PasswordRepromptService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
this.showLowKdf = await this.isLowKdfIteration();
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
: "trashCleanupWarning"
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap(async (params: Params) => {
|
||||
await this.syncService.fullSync(false);
|
||||
await this.vaultFilterService.reloadCollections();
|
||||
await this.vaultItemsComponent.reload();
|
||||
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
if (!cipherId) {
|
||||
return;
|
||||
}
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
if (params.action === "clone") {
|
||||
await this.cloneCipher(cipherView);
|
||||
} else if (params.action === "edit") {
|
||||
await this.editCipher(cipherView);
|
||||
}
|
||||
}),
|
||||
switchMap(() => this.route.queryParams),
|
||||
switchMap(async (params) => {
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
if (cipherId) {
|
||||
if ((await this.cipherService.get(cipherId)) != null) {
|
||||
this.editCipherId(cipherId);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("unknownCipher")
|
||||
);
|
||||
this.router.navigate([], {
|
||||
queryParams: { itemId: null, cipherId: null },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.vaultFilterService.reloadCollections(),
|
||||
this.vaultItemsComponent.load(this.vaultItemsComponent.filter),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get isShowingCards() {
|
||||
return (
|
||||
this.showBrowserOutdated ||
|
||||
this.showPremiumCallout ||
|
||||
this.showUpdateKey ||
|
||||
this.showVerifyEmail ||
|
||||
this.showLowKdf
|
||||
);
|
||||
}
|
||||
|
||||
emailVerified(verified: boolean) {
|
||||
this.showVerifyEmail = !verified;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async applyVaultFilter(filter: VaultFilter) {
|
||||
this.activeFilter = filter;
|
||||
this.vaultItemsComponent.showAddNew = !this.activeFilter.isDeleted;
|
||||
await this.vaultItemsComponent.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
this.activeFilter.isDeleted
|
||||
);
|
||||
this.go();
|
||||
}
|
||||
|
||||
async applyOrganizationFilter(orgId: string) {
|
||||
if (orgId == null) {
|
||||
orgId = "MyVault";
|
||||
}
|
||||
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
|
||||
const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode<OrganizationFilter>;
|
||||
this.filterComponent.filters?.organizationFilter?.action(orgNode);
|
||||
}
|
||||
|
||||
addFolder = async (): Promise<void> => {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
FolderAddEditComponent,
|
||||
this.folderAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.folderId = null;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onSavedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
FolderAddEditComponent,
|
||||
this.folderAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.folderId = folder.id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onSavedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onDeletedFolder.subscribe(async () => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.vaultItemsComponent.searchText = searchText;
|
||||
this.vaultItemsComponent.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 = this.organizationService.get(cipher.organizationId);
|
||||
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
|
||||
this.messagingService.send("upgradeOrganization", {
|
||||
organizationId: cipher.organizationId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
AttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onReuploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
modal.onClosed.subscribe(async () => {
|
||||
if (madeAttachmentChanges) {
|
||||
await this.vaultItemsComponent.refresh();
|
||||
}
|
||||
madeAttachmentChanges = false;
|
||||
});
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
ShareComponent,
|
||||
this.shareModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onSharedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = cipher.id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onSavedCollections.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
const component = await this.editCipher(null);
|
||||
component.type = this.activeFilter.cipherType;
|
||||
if (this.activeFilter.organizationId !== "MyVault") {
|
||||
component.organizationId = this.activeFilter.organizationId;
|
||||
component.collections = (
|
||||
await firstValueFrom(this.vaultFilterService.filteredCollections$)
|
||||
).filter((c) => !c.readOnly && c.id != null);
|
||||
}
|
||||
const selectedColId = this.activeFilter.collectionId;
|
||||
if (selectedColId !== "AllCollections") {
|
||||
component.collectionIds = [selectedColId];
|
||||
}
|
||||
component.folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
async navigateToCipher(cipher: CipherView) {
|
||||
this.go({ itemId: cipher?.id });
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
return this.editCipherId(cipher?.id);
|
||||
}
|
||||
|
||||
async editCipherId(id: string) {
|
||||
const cipher = await this.cipherService.get(id);
|
||||
if (cipher != null && cipher.reprompt != 0) {
|
||||
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null, itemId: null });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
AddEditComponent,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp) => {
|
||||
comp.cipherId = id;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onSavedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onDeletedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
comp.onRestoredCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
modal.onClosedPromise().then(() => {
|
||||
this.go({ cipherId: null, itemId: null });
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
const component = await this.editCipher(cipher);
|
||||
component.cloneMode = true;
|
||||
}
|
||||
|
||||
async updateKey() {
|
||||
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
|
||||
}
|
||||
|
||||
async isLowKdfIteration() {
|
||||
const kdfType = await this.stateService.getKdfType();
|
||||
const kdfOptions = await this.stateService.getKdfConfig();
|
||||
return kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < DEFAULT_PBKDF2_ITERATIONS;
|
||||
}
|
||||
|
||||
get breadcrumbs(): TreeNode<CollectionFilter>[] {
|
||||
if (!this.activeFilter.selectedCollectionNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collections = [this.activeFilter.selectedCollectionNode];
|
||||
while (collections[collections.length - 1].parent != undefined) {
|
||||
collections.push(collections[collections.length - 1].parent);
|
||||
}
|
||||
|
||||
return collections.map((c) => c).reverse();
|
||||
}
|
||||
|
||||
protected applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
|
||||
const filter = this.activeFilter;
|
||||
filter.resetFilter();
|
||||
filter.selectedCollectionNode = collection;
|
||||
this.applyVaultFilter(filter);
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
favorites: this.activeFilter.isFavorites || null,
|
||||
type: this.activeFilter.cipherType,
|
||||
folderId: this.activeFilter.folderId,
|
||||
collectionId: this.activeFilter.collectionId,
|
||||
deleted: this.activeFilter.isDeleted || null,
|
||||
};
|
||||
}
|
||||
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows backwards compatibility with
|
||||
* old links that used the original `cipherId` param
|
||||
*/
|
||||
const getCipherIdFromParams = (params: Params): string => {
|
||||
return params["itemId"] || params["cipherId"];
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BreadcrumbsModule } from "@bitwarden/components";
|
||||
|
||||
import { CollectionBadgeModule } from "../organizations/vault/collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "../organizations/vault/group-badge/group-badge.module";
|
||||
import { SharedModule, LooseComponentsModule } from "../shared";
|
||||
|
||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "./pipes/pipes.module";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
VaultFilterModule,
|
||||
VaultRoutingModule,
|
||||
OrganizationBadgeModule,
|
||||
GroupBadgeModule,
|
||||
CollectionBadgeModule,
|
||||
PipesModule,
|
||||
SharedModule,
|
||||
LooseComponentsModule,
|
||||
BulkDialogsModule,
|
||||
BreadcrumbsModule,
|
||||
],
|
||||
declarations: [VaultComponent, VaultItemsComponent],
|
||||
exports: [VaultComponent],
|
||||
})
|
||||
export class VaultModule {}
|
||||
Reference in New Issue
Block a user