mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
[PM-18520] - Update desktop cipher forms to use the same UI as web app and extension - (#13992)
* WIP - cipher form refactor * cipher clone * cipher clone * finalize item view and form changes * fix tests * hide changes behind feature flag * set flag to false * create vault items v2. add button selector * revert change to flag and vault items * add attachments * revert change to tsconfig * move module * fix modules * cleanup * fix import * fix import * fix import * remove showForm * update feature flag * wip - cleanup * fix up services * cleanup * fix type errors * fix lint errors * add dialog component * revert changes to menu * revert changes to menu * fix vault-items-v2 * set feature flag to FALSE * add missing i18n keys. fix collection state * remove generator. update modules. bug fix * fix restricted imports * mark method as deprecated. add uri arg back * fix shared.module * fix shared.module * fix shared.module * add uri * check and prompt for premium when opening attachments dialog * move VaultItemDialogResult back * fix import in spec file * update copy functions * fix MP reprompt issue
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
tdeDecryptionRequiredGuard,
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
@@ -53,6 +55,7 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
@@ -132,11 +135,15 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "vault",
|
||||
component: VaultComponent,
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: VaultComponent,
|
||||
flaggedComponent: VaultV2Component,
|
||||
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
|
||||
routeOptions: {
|
||||
path: "vault",
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
},
|
||||
}),
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
{
|
||||
@@ -359,7 +366,7 @@ const routes: Routes = [
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
useHash: true,
|
||||
/*enableTracing: true,*/
|
||||
// enableTracing: true,
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -9,7 +9,6 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
@@ -28,6 +27,7 @@ import { PasswordHistoryComponent } from "../vault/app/vault/password-history.co
|
||||
import { ShareComponent } from "../vault/app/vault/share.component";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultItemsComponent } from "../vault/app/vault/vault-items.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component";
|
||||
import { ViewComponent } from "../vault/app/vault/view.component";
|
||||
@@ -55,8 +55,8 @@ import { SharedModule } from "./shared/shared.module";
|
||||
CalloutModule,
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
NavComponent,
|
||||
VaultV2Component,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
@@ -65,7 +65,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
AddEditCustomFieldsComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
VaultItemsComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
@@ -80,9 +79,10 @@ import { SharedModule } from "./shared/shared.module";
|
||||
ShareComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultComponent,
|
||||
VaultItemsComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
ViewComponent,
|
||||
ViewCustomFieldsComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -393,6 +393,64 @@
|
||||
"authenticatorKeyTotp": {
|
||||
"message": "Authenticator key (TOTP)"
|
||||
},
|
||||
"authenticatorKey": {
|
||||
"message": "Authenticator key"
|
||||
},
|
||||
"autofillOptions": {
|
||||
"message": "Autofill options"
|
||||
},
|
||||
"websiteUri": {
|
||||
"message": "Website (URI)"
|
||||
},
|
||||
"websiteUriCount": {
|
||||
"message": "Website (URI) $COUNT$",
|
||||
"description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"websiteAdded": {
|
||||
"message": "Website added"
|
||||
},
|
||||
"addWebsite": {
|
||||
"message": "Add website"
|
||||
},
|
||||
"deleteWebsite": {
|
||||
"message": "Delete website"
|
||||
},
|
||||
"owner": {
|
||||
"message": "Owner"
|
||||
},
|
||||
"addField": {
|
||||
"message": "Add field"
|
||||
},
|
||||
"fieldType": {
|
||||
"message": "Field type"
|
||||
},
|
||||
"fieldLabel": {
|
||||
"message": "Field label"
|
||||
},
|
||||
"add": {
|
||||
"message": "Add"
|
||||
},
|
||||
"textHelpText": {
|
||||
"message": "Use text fields for data like security questions"
|
||||
},
|
||||
"hiddenHelpText": {
|
||||
"message": "Use hidden fields for sensitive data like a password"
|
||||
},
|
||||
"checkBoxHelpText": {
|
||||
"message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email"
|
||||
},
|
||||
"linkedHelpText": {
|
||||
"message": "Use a linked field when you are experiencing autofill issues for a specific website."
|
||||
},
|
||||
"linkedLabelHelpText": {
|
||||
"message": "Enter the the field's html id, name, aria-label, or placeholder."
|
||||
},
|
||||
"folder": {
|
||||
"message": "Folder"
|
||||
},
|
||||
@@ -418,6 +476,9 @@
|
||||
"message": "Linked",
|
||||
"description": "This describes a field that is 'linked' (related) to another field."
|
||||
},
|
||||
"cfTypeCheckbox": {
|
||||
"message": "Checkbox"
|
||||
},
|
||||
"linkedValue": {
|
||||
"message": "Linked value",
|
||||
"description": "This describes a value that is 'linked' (related) to another value."
|
||||
@@ -1915,6 +1976,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cardDetails": {
|
||||
"message": "Card details"
|
||||
},
|
||||
"cardBrandDetails": {
|
||||
"message": "$BRAND$ details",
|
||||
"placeholders": {
|
||||
"brand": {
|
||||
"content": "$1",
|
||||
"example": "Visa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"learnMoreAboutAuthenticators": {
|
||||
"message": "Learn more about authenticators"
|
||||
},
|
||||
"copyTOTP": {
|
||||
"message": "Copy Authenticator key (TOTP)"
|
||||
},
|
||||
"totpHelperTitle": {
|
||||
"message": "Make 2-step verification seamless"
|
||||
},
|
||||
"totpHelper": {
|
||||
"message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field."
|
||||
},
|
||||
"totpHelperWithCapture": {
|
||||
"message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field."
|
||||
},
|
||||
"premium": {
|
||||
"message": "Premium",
|
||||
"description": "Premium membership"
|
||||
},
|
||||
"cardExpiredTitle": {
|
||||
"message": "Expired card"
|
||||
},
|
||||
"cardExpiredMessage": {
|
||||
"message": "If you've renewed it, update the card's information"
|
||||
},
|
||||
"verificationRequired": {
|
||||
"message": "Verification required",
|
||||
"description": "Default title for the user verification dialog."
|
||||
@@ -2084,6 +2182,15 @@
|
||||
"personalOwnershipPolicyInEffectImports": {
|
||||
"message": "An organization policy has blocked importing items into your individual vault."
|
||||
},
|
||||
"personalDetails": {
|
||||
"message": "Personal details"
|
||||
},
|
||||
"identification": {
|
||||
"message": "Identification"
|
||||
},
|
||||
"contactInfo": {
|
||||
"message": "Contact information"
|
||||
},
|
||||
"allSends": {
|
||||
"message": "All Sends",
|
||||
"description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@@ -2518,6 +2625,9 @@
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
},
|
||||
"usernameGenerator": {
|
||||
"message": "Username generator"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
@@ -3439,6 +3549,17 @@
|
||||
"ssoError": {
|
||||
"message": "No free ports could be found for the sso login."
|
||||
},
|
||||
"securePasswordGenerated": {
|
||||
"message": "Secure password generated! Don't forget to also update your password on the website."
|
||||
},
|
||||
"useGeneratorHelpTextPartOne": {
|
||||
"message": "Use the generator",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"useGeneratorHelpTextPartTwo": {
|
||||
"message": "to create a strong unique password",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"biometricsStatusHelptextUnlockNeeded": {
|
||||
"message": "Biometric unlock is unavailable because PIN or password unlock is required first."
|
||||
},
|
||||
@@ -3517,6 +3638,27 @@
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"itemDetails": {
|
||||
"message": "Item details"
|
||||
},
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"loginCredentials": {
|
||||
"message": "Login credentials"
|
||||
},
|
||||
"additionalOptions": {
|
||||
"message": "Additional options"
|
||||
},
|
||||
"itemHistory": {
|
||||
"message": "Item history"
|
||||
},
|
||||
"lastEdited": {
|
||||
"message": "Last edited"
|
||||
},
|
||||
"upload": {
|
||||
"message": "Upload"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
|
||||
@@ -147,3 +147,8 @@ div:not(.modal)::-webkit-scrollbar-thumb,
|
||||
.mx-auto {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.vault-v2 button:not([bitbutton]):not([biticonbutton]) i.bwi,
|
||||
a i.bwi {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -162,3 +162,7 @@ app-root {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vault-v2 > .details {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormGenerationService } from "@bitwarden/vault";
|
||||
|
||||
import { CredentialGeneratorDialogComponent } from "../vault/app/vault/credential-generator-dialog.component";
|
||||
|
||||
@Injectable()
|
||||
export class DesktopCredentialGenerationService implements CipherFormGenerationService {
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
async generatePassword(): Promise<string> {
|
||||
return await this.generateCredential("password");
|
||||
}
|
||||
|
||||
async generateUsername(uri: string): Promise<string> {
|
||||
return await this.generateCredential("username", uri);
|
||||
}
|
||||
|
||||
async generateCredential(type: "password" | "username", uri?: string): Promise<string> {
|
||||
const dialogRef = CredentialGeneratorDialogComponent.open(this.dialogService, { type, uri });
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (!result || result.action === "canceled" || !result.generatedValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return result.generatedValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service";
|
||||
|
||||
describe("DesktopPremiumUpgradePromptService", () => {
|
||||
let service: DesktopPremiumUpgradePromptService;
|
||||
let messager: MockProxy<MessagingService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
messager = mock<MessagingService>();
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopPremiumUpgradePromptService,
|
||||
{ provide: MessagingService, useValue: messager },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
service = TestBed.inject(DesktopPremiumUpgradePromptService);
|
||||
});
|
||||
|
||||
describe("promptForPremium", () => {
|
||||
it("navigates to the premium update screen", async () => {
|
||||
await service.promptForPremium();
|
||||
expect(messager.send).toHaveBeenCalledWith("openPremium");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the desktop.
|
||||
*/
|
||||
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
|
||||
private messagingService = inject(MessagingService);
|
||||
|
||||
async promptForPremium() {
|
||||
this.messagingService.send("openPremium");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<ng-container bitDialogContent>
|
||||
<vault-cipher-form-generator
|
||||
[type]="data.type"
|
||||
[uri]="data.uri"
|
||||
(valueGenerated)="onCredentialGenerated($event)"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
/>
|
||||
@@ -27,7 +28,6 @@
|
||||
(click)="applyCredentials()"
|
||||
appA11yTitle="{{ buttonLabel }}"
|
||||
bitButton
|
||||
bitDialogClose
|
||||
[disabled]="!(buttonLabel && credentialValue)"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DialogService,
|
||||
ItemModule,
|
||||
LinkModule,
|
||||
DialogRef,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorHistoryDialogComponent,
|
||||
@@ -19,10 +20,22 @@ import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
type CredentialGeneratorParams = {
|
||||
onCredentialGenerated: (value?: string) => void;
|
||||
/** @deprecated Prefer use of dialogRef.closed to retreive the generated value */
|
||||
onCredentialGenerated?: (value?: string) => void;
|
||||
type: "password" | "username";
|
||||
uri?: string;
|
||||
};
|
||||
|
||||
export interface CredentialGeneratorDialogResult {
|
||||
action: CredentialGeneratorDialogAction;
|
||||
generatedValue?: string;
|
||||
}
|
||||
|
||||
export enum CredentialGeneratorDialogAction {
|
||||
Selected = "selected",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "credential-generator-dialog",
|
||||
@@ -45,6 +58,7 @@ export class CredentialGeneratorDialogComponent {
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
|
||||
private dialogService: DialogService,
|
||||
private dialogRef: DialogRef<CredentialGeneratorDialogResult>,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@@ -59,11 +73,15 @@ export class CredentialGeneratorDialogComponent {
|
||||
};
|
||||
|
||||
applyCredentials = () => {
|
||||
this.data.onCredentialGenerated(this.credentialValue);
|
||||
this.data.onCredentialGenerated?.(this.credentialValue);
|
||||
this.dialogRef.close({
|
||||
action: CredentialGeneratorDialogAction.Selected,
|
||||
generatedValue: this.credentialValue,
|
||||
});
|
||||
};
|
||||
|
||||
clearCredentials = () => {
|
||||
this.data.onCredentialGenerated();
|
||||
this.data.onCredentialGenerated?.();
|
||||
};
|
||||
|
||||
onCredentialGenerated = (value: string) => {
|
||||
@@ -75,9 +93,12 @@ export class CredentialGeneratorDialogComponent {
|
||||
this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
|
||||
};
|
||||
|
||||
static open = (dialogService: DialogService, data: CredentialGeneratorParams) => {
|
||||
dialogService.open(CredentialGeneratorDialogComponent, {
|
||||
data,
|
||||
});
|
||||
};
|
||||
static open(dialogService: DialogService, data: CredentialGeneratorParams) {
|
||||
return dialogService.open<CredentialGeneratorDialogResult, CredentialGeneratorParams>(
|
||||
CredentialGeneratorDialogComponent,
|
||||
{
|
||||
data,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
64
apps/desktop/src/vault/app/vault/item-footer.component.html
Normal file
64
apps/desktop/src/vault/app/vault/item-footer.component.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<div class="footer">
|
||||
<ng-container *ngIf="!cipher.decryptionFailure">
|
||||
<button
|
||||
#submitBtn
|
||||
bitButton
|
||||
form="cipherForm"
|
||||
type="submit"
|
||||
*ngIf="action !== 'view'"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-save-changes bwi-lg bwi-fw" [hidden]="isSubmitting" aria-hidden="true"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!isSubmitting"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted && action === 'view'"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="action === 'edit' || action === 'clone' || action === 'add'"
|
||||
type="button"
|
||||
(click)="cancel()"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="cipher.id && !cipher?.organizationId && !cipher.isDeleted && action !== 'clone'"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="((canDeleteCipher$ | async) && action === 'edit') || action === 'view'">
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
159
apps/desktop/src/vault/app/vault/item-footer.component.ts
Normal file
159
apps/desktop/src/vault/app/vault/item-footer.component.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-item-footer",
|
||||
templateUrl: "item-footer.component.html",
|
||||
standalone: true,
|
||||
imports: [ButtonModule, CommonModule, JslibModule],
|
||||
})
|
||||
export class ItemFooterComponent implements OnInit {
|
||||
@Input({ required: true }) cipher: CipherView = new CipherView();
|
||||
@Input() collectionId: string | null = null;
|
||||
@Input({ required: true }) action: string = "view";
|
||||
@Input() isSubmitting: boolean = false;
|
||||
@Output() onEdit = new EventEmitter<CipherView>();
|
||||
@Output() onClone = new EventEmitter<CipherView>();
|
||||
@Output() onDelete = new EventEmitter<CipherView>();
|
||||
@Output() onRestore = new EventEmitter<CipherView>();
|
||||
@Output() onCancel = new EventEmitter<CipherView>();
|
||||
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
|
||||
|
||||
canDeleteCipher$: Observable<boolean> = new Observable();
|
||||
activeUserId: UserId | null = null;
|
||||
|
||||
private passwordReprompted = false;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected dialogService: DialogService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
protected i18nService: I18nService,
|
||||
protected logService: LogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||
this.collectionId as CollectionId,
|
||||
]);
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
}
|
||||
|
||||
async clone() {
|
||||
if (this.cipher.login?.hasFido2Credentials) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (await this.promptPassword()) {
|
||||
this.onClone.emit(this.cipher);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected edit() {
|
||||
this.onEdit.emit(this.cipher);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCancel.emit(this.cipher);
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
if (!(await this.promptPassword())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.deleteCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
|
||||
),
|
||||
});
|
||||
this.onDelete.emit(this.cipher);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async restore(): Promise<boolean> {
|
||||
if (!this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.restoreCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("restoredItem"),
|
||||
});
|
||||
this.onRestore.emit(this.cipher);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected async promptPassword() {
|
||||
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<div class="container loading-spinner" *ngIf="!loaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="loaded">
|
||||
<div class="content">
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="42"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers.length"
|
||||
>
|
||||
<div class="list">
|
||||
<button
|
||||
type="button"
|
||||
*cdkVirtualFor="let c of ciphers; trackBy: trackByFn"
|
||||
appStopClick
|
||||
(click)="selectCipher(c)"
|
||||
(contextmenu)="rightClickCipher(c)"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
[ngClass]="{ active: c.id === activeCipherId }"
|
||||
[attr.aria-pressed]="c.id === activeCipherId"
|
||||
class="flex-list-item virtual-scroll-item"
|
||||
>
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
<div class="flex-cipher-list-item">
|
||||
<span class="text">
|
||||
<span class="truncate-box">
|
||||
<span class="truncate">{{ c.name }}</span>
|
||||
<ng-container *ngIf="c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection text-muted"
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip text-muted"
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<div class="no-items" *ngIf="!ciphers.length">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<ng-container *ngTemplateOutlet="addCipherButton"></ng-container>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ng-container *ngTemplateOutlet="addCipherButton"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #addCipherButton>
|
||||
<button
|
||||
type="button"
|
||||
class="block primary"
|
||||
bitButton
|
||||
appA11yTitle="{{ 'addItem' | i18n }}"
|
||||
[disabled]="deleted"
|
||||
[bitMenuTriggerFor]="addCipherMenu"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addCipherMenu>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
||||
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
||||
<i class="bwi bwi-credit-card tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
||||
<i class="bwi bwi-id-card tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeSecureNote" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</ng-template>
|
||||
42
apps/desktop/src/vault/app/vault/vault-items-v2.component.ts
Normal file
42
apps/desktop/src/vault/app/vault/vault-items-v2.component.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { distinctUntilChanged } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { MenuModule } from "@bitwarden/components";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items-v2",
|
||||
templateUrl: "vault-items-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [MenuModule, CommonModule, JslibModule, ScrollingModule],
|
||||
})
|
||||
export class VaultItemsV2Component extends BaseVaultItemsComponent {
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
private readonly searchBarService: SearchBarService,
|
||||
cipherService: CipherService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(searchService, cipherService, accountService);
|
||||
|
||||
this.searchBarService.searchText$
|
||||
.pipe(distinctUntilChanged(), takeUntilDestroyed())
|
||||
.subscribe((searchText) => {
|
||||
this.searchText = searchText!;
|
||||
});
|
||||
}
|
||||
|
||||
trackByFn(index: number, c: CipherView): string {
|
||||
return c.id;
|
||||
}
|
||||
}
|
||||
80
apps/desktop/src/vault/app/vault/vault-v2.component.html
Normal file
80
apps/desktop/src/vault/app/vault/vault-v2.component.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
|
||||
<app-vault-items-v2
|
||||
id="items"
|
||||
class="items"
|
||||
[activeCipherId]="cipherId"
|
||||
(onCipherClicked)="viewCipher($event)"
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCipherOptions)="addCipherOptions()"
|
||||
>
|
||||
</app-vault-items-v2>
|
||||
<div class="details" *ngIf="!!action">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher"
|
||||
[action]="action"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher()"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
<app-cipher-view *ngIf="action === 'view'" [cipher]="cipher" [collections]="collections">
|
||||
</app-cipher-view>
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
*ngIf="action === 'add' || action === 'edit' || action === 'clone'"
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
|
||||
<p class="tw-m-0">
|
||||
{{ "attachments" | i18n }}
|
||||
<span
|
||||
*ngIf="!(canAccessAttachments$ | async)"
|
||||
bitBadge
|
||||
variant="success"
|
||||
class="tw-ml-2"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="logo"
|
||||
class="logo"
|
||||
*ngIf="action !== 'add' && action !== 'edit' && action !== 'view' && action !== 'clone'"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav">
|
||||
<app-vault-filter
|
||||
class="vault-filters"
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event.id)"
|
||||
></app-vault-filter>
|
||||
<app-nav class="nav"></app-nav>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
785
apps/desktop/src/vault/app/vault/vault-v2.component.ts
Normal file
785
apps/desktop/src/vault/app/vault/vault-v2.component.ts
Normal file
@@ -0,0 +1,785 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
ItemModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
ChangeLoginPasswordService,
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormGenerationService,
|
||||
CipherFormMode,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { NavComponent } from "../../../app/layout/nav.component";
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service";
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
|
||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { ItemFooterComponent } from "./item-footer.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultItemsV2Component } from "./vault-items-v2.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "vault-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
BadgeModule,
|
||||
CommonModule,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
ItemFooterComponent,
|
||||
I18nPipe,
|
||||
ItemModule,
|
||||
ButtonModule,
|
||||
NavComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsV2Component,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherFormConfigService,
|
||||
useClass: DefaultCipherFormConfigService,
|
||||
},
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
useClass: DefaultChangeLoginPasswordService,
|
||||
},
|
||||
{
|
||||
provide: ViewPasswordHistoryService,
|
||||
useClass: VaultViewPasswordHistoryService,
|
||||
},
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
{ provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService },
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
@ViewChild(VaultItemsV2Component, { static: true })
|
||||
vaultItemsComponent: VaultItemsV2Component | null = null;
|
||||
@ViewChild(VaultFilterComponent, { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent | null = null;
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
folderAddEditModalRef: ViewContainerRef | null = null;
|
||||
|
||||
action: CipherFormMode | "view" | null = null;
|
||||
cipherId: string | null = null;
|
||||
favorites = false;
|
||||
type: CipherType | null = null;
|
||||
folderId: string | null = null;
|
||||
collectionId: string | null = null;
|
||||
organizationId: string | null = null;
|
||||
myVaultOnly = false;
|
||||
addType: CipherType | undefined = undefined;
|
||||
addOrganizationId: string | null = null;
|
||||
addCollectionIds: string[] | null = null;
|
||||
showingModal = false;
|
||||
deleted = false;
|
||||
userHasPremiumAccess = false;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
activeUserId: UserId | null = null;
|
||||
cipherRepromptId: string | null = null;
|
||||
cipher: CipherView | null = new CipherView();
|
||||
collections: CollectionView[] | null = null;
|
||||
config: CipherFormConfig | null = null;
|
||||
|
||||
protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
private modal: ModalRef | null = null;
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
private syncService: SyncService,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private totpService: TotpService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private searchBarService: SearchBarService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
private formConfigService: CipherFormConfigService,
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((canAccessPremium: boolean) => {
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone
|
||||
.run(async () => {
|
||||
let detectChanges = true;
|
||||
try {
|
||||
switch (message.command) {
|
||||
case "newLogin":
|
||||
await this.addCipher(CipherType.Login).catch(() => {});
|
||||
break;
|
||||
case "newCard":
|
||||
await this.addCipher(CipherType.Card).catch(() => {});
|
||||
break;
|
||||
case "newIdentity":
|
||||
await this.addCipher(CipherType.Identity).catch(() => {});
|
||||
break;
|
||||
case "newSecureNote":
|
||||
await this.addCipher(CipherType.SecureNote).catch(() => {});
|
||||
break;
|
||||
case "focusSearch":
|
||||
(document.querySelector("#search") as HTMLInputElement)?.select();
|
||||
detectChanges = false;
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent
|
||||
.reload(this.activeFilter.buildFilter())
|
||||
.catch(() => {});
|
||||
}
|
||||
if (this.vaultFilterComponent) {
|
||||
await this.vaultFilterComponent
|
||||
.reloadCollectionsAndFolders(this.activeFilter)
|
||||
.catch(() => {});
|
||||
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
this.showingModal = true;
|
||||
break;
|
||||
case "modalClosed":
|
||||
this.showingModal = false;
|
||||
break;
|
||||
case "copyUsername": {
|
||||
if (this.cipher?.login?.username) {
|
||||
this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copyPassword": {
|
||||
if (this.cipher?.login?.password && this.cipher.viewPassword) {
|
||||
this.copyValue(this.cipher, this.cipher.login.password, "password", "Password");
|
||||
await this.eventCollectionService
|
||||
.collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id)
|
||||
.catch(() => {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copyTotp": {
|
||||
if (
|
||||
this.cipher?.login?.hasTotp &&
|
||||
(this.cipher.organizationUseTotp || this.userHasPremiumAccess)
|
||||
) {
|
||||
const value = await firstValueFrom(
|
||||
this.totpService.getCode$(this.cipher.login.totp),
|
||||
).catch(() => null);
|
||||
if (value) {
|
||||
this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
detectChanges = false;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
if (detectChanges) {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
await this.load().catch(() => {});
|
||||
}
|
||||
|
||||
this.searchBarService.setEnabled(true);
|
||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||
|
||||
const authRequest = await this.apiService.getLastAuthRequest().catch(() => null);
|
||||
if (authRequest != null) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
}
|
||||
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
).catch(() => null);
|
||||
|
||||
if (this.activeUserId) {
|
||||
this.cipherService
|
||||
.failedToDecryptCiphers$(this.activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchBarService.setEnabled(false);
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.componentIsDestroyed$.next(true);
|
||||
this.componentIsDestroyed$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
const params = await firstValueFrom(this.route.queryParams).catch();
|
||||
if (params.cipherId) {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = params.cipherId;
|
||||
if (params.action === "clone") {
|
||||
await this.cloneCipher(cipherView).catch(() => {});
|
||||
} else if (params.action === "edit") {
|
||||
await this.editCipher(cipherView).catch(() => {});
|
||||
} else {
|
||||
await this.viewCipher(cipherView).catch(() => {});
|
||||
}
|
||||
} else if (params.action === "add") {
|
||||
this.addType = Number(params.addType);
|
||||
await this.addCipher(this.addType).catch(() => {});
|
||||
}
|
||||
|
||||
this.activeFilter = new VaultFilter({
|
||||
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
|
||||
cipherType:
|
||||
params.action === "add" || params.type == null
|
||||
? undefined
|
||||
: (parseInt(params.type) as CipherType),
|
||||
selectedFolderId: params.folderId,
|
||||
selectedCollectionId: params.selectedCollectionId,
|
||||
selectedOrganizationId: params.selectedOrganizationId,
|
||||
myVaultOnly: params.myVaultOnly ?? false,
|
||||
});
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async viewCipher(cipher: CipherView) {
|
||||
if (await this.shouldReprompt(cipher, "view")) {
|
||||
return;
|
||||
}
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
this.collections =
|
||||
this.vaultFilterComponent?.collections.fullList.filter((c) =>
|
||||
cipher.collectionIds.includes(c.id),
|
||||
) ?? null;
|
||||
this.action = "view";
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
async openAttachmentsDialog() {
|
||||
if (!this.userHasPremiumAccess) {
|
||||
await this.premiumUpgradePromptService.promptForPremium();
|
||||
return;
|
||||
}
|
||||
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
|
||||
cipherId: this.cipherId as CipherId,
|
||||
});
|
||||
const result = await firstValueFrom(dialogRef.closed).catch(() => null);
|
||||
if (
|
||||
result?.action === AttachmentDialogResult.Removed ||
|
||||
result?.action === AttachmentDialogResult.Uploaded
|
||||
) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
viewCipherMenu(cipher: CipherView) {
|
||||
const menu: RendererMenuItem[] = [
|
||||
{
|
||||
label: this.i18nService.t("view"),
|
||||
click: () => {
|
||||
this.functionWithChangeDetection(() => {
|
||||
this.viewCipher(cipher).catch(() => {});
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (cipher.decryptionFailure) {
|
||||
invokeMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cipher.isDeleted) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("edit"),
|
||||
click: () => {
|
||||
this.functionWithChangeDetection(() => {
|
||||
this.editCipher(cipher).catch(() => {});
|
||||
});
|
||||
},
|
||||
});
|
||||
if (!cipher.organizationId) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("clone"),
|
||||
click: () => {
|
||||
this.functionWithChangeDetection(() => {
|
||||
this.cloneCipher(cipher).catch(() => {});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (cipher.type) {
|
||||
case CipherType.Login:
|
||||
if (
|
||||
cipher.login.canLaunch ||
|
||||
cipher.login.username != null ||
|
||||
cipher.login.password != null
|
||||
) {
|
||||
menu.push({ type: "separator" });
|
||||
}
|
||||
if (cipher.login.canLaunch) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("launch"),
|
||||
click: () => this.platformUtilsService.launchUri(cipher.login.launchUri),
|
||||
});
|
||||
}
|
||||
if (cipher.login.username != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyUsername"),
|
||||
click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"),
|
||||
});
|
||||
}
|
||||
if (cipher.login.password != null && cipher.viewPassword) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyPassword"),
|
||||
click: () => {
|
||||
this.copyValue(cipher, cipher.login.password, "password", "Password");
|
||||
this.eventCollectionService
|
||||
.collect(EventType.Cipher_ClientCopiedPassword, cipher.id)
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyVerificationCodeTotp"),
|
||||
click: async () => {
|
||||
const value = await firstValueFrom(
|
||||
this.totpService.getCode$(cipher.login.totp),
|
||||
).catch(() => null);
|
||||
if (value) {
|
||||
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CipherType.Card:
|
||||
if (cipher.card.number != null || cipher.card.code != null) {
|
||||
menu.push({ type: "separator" });
|
||||
}
|
||||
if (cipher.card.number != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyNumber"),
|
||||
click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"),
|
||||
});
|
||||
}
|
||||
if (cipher.card.code != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copySecurityCode"),
|
||||
click: () => {
|
||||
this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code");
|
||||
this.eventCollectionService
|
||||
.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id)
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
invokeMenu(menu);
|
||||
}
|
||||
|
||||
async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise<boolean> {
|
||||
return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher));
|
||||
}
|
||||
|
||||
async buildFormConfig(action: CipherFormMode) {
|
||||
this.config = await this.formConfigService
|
||||
.buildConfig(action, this.cipherId as CipherId, this.addType)
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
if (await this.shouldReprompt(cipher, "edit")) {
|
||||
return;
|
||||
}
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
await this.buildFormConfig("edit");
|
||||
this.action = "edit";
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
if (await this.shouldReprompt(cipher, "clone")) {
|
||||
return;
|
||||
}
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
await this.buildFormConfig("clone");
|
||||
this.action = "clone";
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
async addCipher(type: CipherType) {
|
||||
this.addType = type || this.activeFilter.cipherType;
|
||||
this.cipherId = null;
|
||||
await this.buildFormConfig("add");
|
||||
this.action = "add";
|
||||
this.prefillCipherFromFilter();
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
addCipherOptions() {
|
||||
const menu: RendererMenuItem[] = [
|
||||
{
|
||||
label: this.i18nService.t("typeLogin"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Login),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeCard"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Card),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeIdentity"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Identity),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeSecureNote"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.SecureNote),
|
||||
},
|
||||
];
|
||||
invokeMenu(menu);
|
||||
}
|
||||
|
||||
async savedCipher(cipher: CipherView) {
|
||||
this.cipherId = null;
|
||||
this.action = "view";
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
if (this.activeUserId) {
|
||||
await this.cipherService.clearCache(this.activeUserId).catch(() => {});
|
||||
}
|
||||
await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {});
|
||||
await this.go().catch(() => {});
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
|
||||
async deleteCipher() {
|
||||
this.cipherId = null;
|
||||
this.cipher = null;
|
||||
this.action = null;
|
||||
await this.go().catch(() => {});
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
|
||||
async restoreCipher() {
|
||||
this.cipherId = null;
|
||||
this.action = null;
|
||||
await this.go().catch(() => {});
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
|
||||
async cancelCipher(cipher: CipherView) {
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
this.action = this.cipherId != null ? "view" : null;
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.searchBarService.setPlaceholderText(
|
||||
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)),
|
||||
);
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.vaultItemsComponent
|
||||
?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash")
|
||||
.catch(() => {});
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
return "searchVault";
|
||||
}
|
||||
|
||||
async addFolder() {
|
||||
this.messagingService.send("newFolder");
|
||||
}
|
||||
|
||||
async editFolder(folderId: string) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
if (this.folderAddEditModalRef == null) {
|
||||
return;
|
||||
}
|
||||
const [modal, childComponent] = await this.modalService
|
||||
.openViewRef(
|
||||
FolderAddEditComponent,
|
||||
this.folderAddEditModalRef,
|
||||
(comp) => (comp.folderId = folderId),
|
||||
)
|
||||
.catch(() => [null, null] as any);
|
||||
this.modal = modal;
|
||||
if (childComponent) {
|
||||
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
|
||||
this.modal?.close();
|
||||
await this.vaultFilterComponent
|
||||
?.reloadCollectionsAndFolders(this.activeFilter)
|
||||
.catch(() => {});
|
||||
});
|
||||
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
|
||||
this.modal?.close();
|
||||
await this.vaultFilterComponent
|
||||
?.reloadCollectionsAndFolders(this.activeFilter)
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
if (this.modal) {
|
||||
this.modal.onClosed.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private dirtyInput(): boolean {
|
||||
return (
|
||||
(this.action === "add" || this.action === "edit" || this.action === "clone") &&
|
||||
document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private async wantsToSaveChanges(): Promise<boolean> {
|
||||
const confirmed = await this.dialogService
|
||||
.openSimpleDialog({
|
||||
title: { key: "unsavedChangesTitle" },
|
||||
content: { key: "unsavedChangesConfirmation" },
|
||||
type: "warning",
|
||||
})
|
||||
.catch(() => false);
|
||||
return !confirmed;
|
||||
}
|
||||
|
||||
private async go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
action: this.action,
|
||||
cipherId: this.cipherId,
|
||||
favorites: this.favorites ? true : null,
|
||||
type: this.type,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
organizationId: this.organizationId,
|
||||
myVaultOnly: this.myVaultOnly,
|
||||
};
|
||||
}
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
replaceUrl: true,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
private addCipherWithChangeDetection(type: CipherType) {
|
||||
this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {}));
|
||||
}
|
||||
|
||||
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
|
||||
this.functionWithChangeDetection(() => {
|
||||
(async () => {
|
||||
if (
|
||||
cipher.reprompt !== CipherRepromptType.None &&
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.passwordReprompt(cipher))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.platformUtilsService.copyToClipboard(value);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: undefined,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)),
|
||||
});
|
||||
if (this.action === "view") {
|
||||
this.messagingService.send("minimizeOnCopy");
|
||||
}
|
||||
})().catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
private functionWithChangeDetection(func: () => void) {
|
||||
this.ngZone.run(() => {
|
||||
func();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
private prefillCipherFromFilter() {
|
||||
if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) {
|
||||
const collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId,
|
||||
);
|
||||
if (collections.length > 0) {
|
||||
this.addOrganizationId = collections[0].organizationId;
|
||||
this.addCollectionIds = [this.activeFilter.selectedCollectionId];
|
||||
}
|
||||
} else if (this.activeFilter.selectedOrganizationId) {
|
||||
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
}
|
||||
}
|
||||
|
||||
private async canNavigateAway(action: string, cipher?: CipherView) {
|
||||
if (this.action === action && (!cipher || this.cipherId === cipher.id)) {
|
||||
return false;
|
||||
} else if (this.dirtyInput() && (await this.wantsToSaveChanges())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async passwordReprompt(cipher: CipherView) {
|
||||
if (cipher.reprompt === CipherRepromptType.None) {
|
||||
this.cipherRepromptId = null;
|
||||
return true;
|
||||
}
|
||||
if (this.cipherRepromptId === cipher.id) {
|
||||
return true;
|
||||
}
|
||||
const repromptResult = await this.passwordRepromptService.showPasswordPrompt();
|
||||
if (repromptResult) {
|
||||
this.cipherRepromptId = cipher.id;
|
||||
}
|
||||
return repromptResult;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user