mirror of
https://github.com/bitwarden/browser
synced 2026-01-27 14:53:44 +00:00
wip - working drawer, footer, and title
This commit is contained in:
@@ -160,6 +160,66 @@
|
||||
"message": "Edit File Send",
|
||||
"description": "Header for edit file send"
|
||||
},
|
||||
"viewItemHeaderLogin": {
|
||||
"message": "View Login",
|
||||
"description": "Header for view login item type"
|
||||
},
|
||||
"viewItemHeaderCard": {
|
||||
"message": "View Card",
|
||||
"description": "Header for view card item type"
|
||||
},
|
||||
"viewItemHeaderIdentity": {
|
||||
"message": "View Identity",
|
||||
"description": "Header for view identity item type"
|
||||
},
|
||||
"viewItemHeaderNote": {
|
||||
"message": "View Note",
|
||||
"description": "Header for view note item type"
|
||||
},
|
||||
"viewItemHeaderSshKey": {
|
||||
"message": "View SSH key",
|
||||
"description": "Header for view SSH key item type"
|
||||
},
|
||||
"newItemHeaderLogin": {
|
||||
"message": "New Login",
|
||||
"description": "Header for new login item type"
|
||||
},
|
||||
"newItemHeaderCard": {
|
||||
"message": "New Card",
|
||||
"description": "Header for new card item type"
|
||||
},
|
||||
"newItemHeaderIdentity": {
|
||||
"message": "New Identity",
|
||||
"description": "Header for new identity item type"
|
||||
},
|
||||
"newItemHeaderNote": {
|
||||
"message": "New Note",
|
||||
"description": "Header for new note item type"
|
||||
},
|
||||
"newItemHeaderSshKey": {
|
||||
"message": "New SSH key",
|
||||
"description": "Header for new SSH key item type"
|
||||
},
|
||||
"editItemHeaderLogin": {
|
||||
"message": "Edit Login",
|
||||
"description": "Header for edit login item type"
|
||||
},
|
||||
"editItemHeaderCard": {
|
||||
"message": "Edit Card",
|
||||
"description": "Header for edit card item type"
|
||||
},
|
||||
"editItemHeaderIdentity": {
|
||||
"message": "Edit Identity",
|
||||
"description": "Header for edit identity item type"
|
||||
},
|
||||
"editItemHeaderNote": {
|
||||
"message": "Edit Note",
|
||||
"description": "Header for edit note item type"
|
||||
},
|
||||
"editItemHeaderSshKey": {
|
||||
"message": "Edit SSH key",
|
||||
"description": "Header for edit SSH key item type"
|
||||
},
|
||||
"deleteSendPermanentConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
|
||||
@@ -166,3 +166,14 @@ app-root {
|
||||
.vault-v2 > .details {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// Vault V3 - Drawer-specific styles
|
||||
.vault-v3 {
|
||||
> .items {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<bit-dialog dialogSize="large" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
@if (showCipherView) {
|
||||
<app-cipher-view [cipher]="cipher" [collections]="collections"> </app-cipher-view>
|
||||
}
|
||||
|
||||
@if (loadForm) {
|
||||
<vault-cipher-form
|
||||
formId="cipherForm"
|
||||
[config]="formConfig"
|
||||
[submitBtn]="itemFooter?.submitBtn"
|
||||
(formReady)="onFormReady()"
|
||||
(cipherSaved)="onCipherSaved($event)"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<app-vault-item-footer
|
||||
[cipher]="cipher"
|
||||
[action]="action"
|
||||
(onEdit)="onEdit()"
|
||||
(onClone)="onClone($event)"
|
||||
(onDelete)="onDelete()"
|
||||
(onRestore)="onRestore()"
|
||||
(onCancel)="onCancel()"
|
||||
(onArchiveToggle)="onArchiveToggle()"
|
||||
>
|
||||
</app-vault-item-footer>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,407 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnDestroy, ViewChild } from "@angular/core";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
// import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
DialogModule,
|
||||
ItemModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
// AttachmentDialogResult,
|
||||
// AttachmentsV2Component,
|
||||
CipherFormComponent,
|
||||
CipherFormConfig,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { ItemFooterComponent } from "../vault/item-footer.component";
|
||||
|
||||
export interface VaultItemDrawerParams {
|
||||
/**
|
||||
* The configuration object for the cipher form.
|
||||
*/
|
||||
config: CipherFormConfig;
|
||||
|
||||
/**
|
||||
* The initial mode for the drawer: 'view' | 'add' | 'edit' | 'clone'
|
||||
*/
|
||||
initialMode: "view" | "add" | "edit" | "clone";
|
||||
}
|
||||
|
||||
/** A result of the vault item drawer. */
|
||||
export const VaultItemDrawerResult = Object.freeze({
|
||||
/** The cipher was saved. */
|
||||
Saved: "saved",
|
||||
/** The cipher was deleted. */
|
||||
Deleted: "deleted",
|
||||
/** The cipher was archived/unarchived. */
|
||||
Archived: "archived",
|
||||
/** The cipher was restored. */
|
||||
Restored: "restored",
|
||||
} as const);
|
||||
|
||||
/** A result of the vault item drawer. */
|
||||
export type VaultItemDrawerResult = {
|
||||
result: (typeof VaultItemDrawerResult)[keyof typeof VaultItemDrawerResult];
|
||||
cipher?: CipherView;
|
||||
};
|
||||
|
||||
type DrawerMode = "view" | "form";
|
||||
|
||||
/**
|
||||
* Component for viewing or editing a vault item in a drawer.
|
||||
* Supports both view and edit modes with in-drawer switching.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "vault-item-drawer.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
AsyncActionsModule,
|
||||
DialogModule,
|
||||
ItemFooterComponent,
|
||||
ItemModule,
|
||||
PremiumBadgeComponent,
|
||||
],
|
||||
})
|
||||
export class VaultItemDrawerComponent implements OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(ItemFooterComponent) itemFooter: ItemFooterComponent | null = null;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CipherFormComponent) cipherFormComponent!: CipherFormComponent;
|
||||
|
||||
/**
|
||||
* The title of the drawer.
|
||||
*/
|
||||
protected title: string;
|
||||
|
||||
/**
|
||||
* Current mode of the drawer: 'view' or 'form'
|
||||
*/
|
||||
protected mode: DrawerMode;
|
||||
|
||||
/**
|
||||
* Flag to initialize/attach the form component.
|
||||
*/
|
||||
protected loadForm: boolean;
|
||||
|
||||
/**
|
||||
* Flag to indicate the form is ready to be displayed.
|
||||
*/
|
||||
protected formReady = false;
|
||||
|
||||
/**
|
||||
* The configuration for the cipher form.
|
||||
*/
|
||||
protected formConfig: CipherFormConfig;
|
||||
|
||||
/**
|
||||
* The cipher being viewed or edited.
|
||||
*/
|
||||
protected cipher: CipherView | null = null;
|
||||
|
||||
/**
|
||||
* Collections the cipher is assigned to.
|
||||
*/
|
||||
protected collections: CollectionView[] = [];
|
||||
|
||||
/**
|
||||
* The action to pass to ItemFooterComponent ('view', 'add', 'edit', 'clone')
|
||||
*/
|
||||
protected get action(): string {
|
||||
if (this.mode === "view") {
|
||||
return "view";
|
||||
}
|
||||
return this.formConfig.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show the cipher view component.
|
||||
*/
|
||||
protected get showCipherView(): boolean {
|
||||
return this.cipher != null && this.mode === "view";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the form is loading (initialized but not ready).
|
||||
*/
|
||||
protected get loadingForm(): boolean {
|
||||
return this.loadForm && !this.formReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks if the cipher was ever modified while the drawer was open.
|
||||
* @private
|
||||
*/
|
||||
private _cipherModified = false;
|
||||
|
||||
/**
|
||||
* Subject to emit when the form is ready to be displayed.
|
||||
* @private
|
||||
*/
|
||||
private _formReadySubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* The initial mode from params (used to determine behavior after save).
|
||||
* @private
|
||||
*/
|
||||
private _initialMode = this.params.initialMode;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: VaultItemDrawerParams,
|
||||
private dialogRef: DialogRef<VaultItemDrawerResult>,
|
||||
private i18nService: I18nService,
|
||||
private dialogService: DialogService,
|
||||
) {
|
||||
this.formConfig = params.config;
|
||||
this.cipher = params.config.originalCipher
|
||||
? new CipherView(params.config.originalCipher)
|
||||
: null;
|
||||
|
||||
if (this.cipher && this.formConfig.collections) {
|
||||
this.collections = this.formConfig.collections.filter((c) =>
|
||||
this.cipher.collectionIds?.includes(c.id),
|
||||
);
|
||||
}
|
||||
|
||||
// Set initial mode
|
||||
this.mode = params.initialMode === "view" ? "view" : "form";
|
||||
this.loadForm = this.mode === "form";
|
||||
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// If the cipher was modified, be sure we emit the saved result in case the dialog was closed with the X button or ESC key.
|
||||
if (this._cipherModified) {
|
||||
this.dialogRef.close({ result: VaultItemDrawerResult.Saved });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the CipherFormComponent when the cipher is saved successfully.
|
||||
*/
|
||||
protected async onCipherSaved(cipherView: CipherView) {
|
||||
this.cipher = cipherView;
|
||||
this._cipherModified = true;
|
||||
|
||||
if (this.formConfig.collections) {
|
||||
this.collections = this.formConfig.collections.filter((c) =>
|
||||
cipherView.collectionIds?.includes(c.id),
|
||||
);
|
||||
}
|
||||
|
||||
// If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits.
|
||||
if (this._initialMode === "add" || this._initialMode === "clone") {
|
||||
this.formConfig.mode = "edit";
|
||||
this.formConfig.initialValues = null;
|
||||
}
|
||||
|
||||
// Switch back to view mode after save
|
||||
await this.changeMode("view");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the CipherFormComponent when the form is ready to be displayed.
|
||||
*/
|
||||
protected onFormReady() {
|
||||
this.formReady = true;
|
||||
this._formReadySubject.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onEdit event.
|
||||
* Switches from view mode to edit mode.
|
||||
*/
|
||||
protected async onEdit() {
|
||||
await this.changeMode("form");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onClone event.
|
||||
*/
|
||||
protected async onClone(cipher: CipherView) {
|
||||
// Close drawer and let parent handle cloning by reopening with clone mode
|
||||
this.dialogRef.close({ result: VaultItemDrawerResult.Saved });
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onDelete event.
|
||||
*/
|
||||
protected onDelete() {
|
||||
this._cipherModified = false;
|
||||
this.dialogRef.close({ result: VaultItemDrawerResult.Deleted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onRestore event.
|
||||
*/
|
||||
protected onRestore() {
|
||||
this._cipherModified = false;
|
||||
this.dialogRef.close({ result: VaultItemDrawerResult.Restored });
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onCancel event.
|
||||
* Returns to view mode or closes drawer if no cipher exists.
|
||||
*/
|
||||
protected async onCancel() {
|
||||
// We're in View mode, we don't have a cipher, or we were adding/cloning, close the drawer.
|
||||
if (
|
||||
this.mode === "view" ||
|
||||
this.cipher == null ||
|
||||
this._initialMode === "add" ||
|
||||
this._initialMode === "clone"
|
||||
) {
|
||||
this.dialogRef.close(
|
||||
this._cipherModified ? { result: VaultItemDrawerResult.Saved } : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We're in Form mode, and we have a cipher, switch back to View mode.
|
||||
await this.changeMode("view");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ItemFooter emits onArchiveToggle event.
|
||||
*/
|
||||
protected async onArchiveToggle() {
|
||||
// ItemFooter handles the archive logic, just refresh and indicate change
|
||||
this._cipherModified = true;
|
||||
await this.refreshCurrentCipher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the attachments dialog for the current cipher.
|
||||
*/
|
||||
protected async openAttachmentsDialog() {
|
||||
// const dialogRef = AttachmentsV2Component.open(this.dialogService, {
|
||||
// cipherId: this.formConfig.originalCipher?.id as CipherId,
|
||||
// organizationId: this.formConfig.originalCipher?.organizationId as OrganizationId,
|
||||
// });
|
||||
// const result = await firstValueFrom(dialogRef.closed);
|
||||
// if (
|
||||
// result.action === AttachmentDialogResult.Removed ||
|
||||
// result.action === AttachmentDialogResult.Uploaded
|
||||
// ) {
|
||||
// // Update the cipher form with the new attachments
|
||||
// this.cipherFormComponent.patchCipher((currentCipher) => {
|
||||
// currentCipher.attachments = result.cipher?.attachments;
|
||||
// currentCipher.revisionDate = result.cipher?.revisionDate;
|
||||
// return currentCipher;
|
||||
// });
|
||||
// this._cipherModified = true;
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the current cipher after an action like archive.
|
||||
* @private
|
||||
*/
|
||||
private async refreshCurrentCipher() {
|
||||
// The cipher should be updated by the ItemFooter's archive action
|
||||
// We just need to mark it as modified
|
||||
this._cipherModified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the mode of the drawer. When switching to Form mode, the form is initialized first then displayed once ready.
|
||||
* @param mode
|
||||
* @private
|
||||
*/
|
||||
private async changeMode(mode: DrawerMode) {
|
||||
this.formReady = false;
|
||||
|
||||
if (mode === "form") {
|
||||
this.loadForm = true;
|
||||
// Wait for the formReadySubject to emit before continuing.
|
||||
// This helps prevent flashing an empty dialog while the form is initializing.
|
||||
await firstValueFrom(this._formReadySubject);
|
||||
} else {
|
||||
this.loadForm = false;
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title based on current mode and cipher type.
|
||||
* @private
|
||||
*/
|
||||
private updateTitle(): void {
|
||||
const translation: { [key: string]: { [key: number]: string } } = {
|
||||
view: {
|
||||
[CipherType.Login]: "viewItemHeaderLogin",
|
||||
[CipherType.Card]: "viewItemHeaderCard",
|
||||
[CipherType.Identity]: "viewItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "viewItemHeaderNote",
|
||||
[CipherType.SshKey]: "viewItemHeaderSshKey",
|
||||
},
|
||||
new: {
|
||||
[CipherType.Login]: "newItemHeaderLogin",
|
||||
[CipherType.Card]: "newItemHeaderCard",
|
||||
[CipherType.Identity]: "newItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "newItemHeaderNote",
|
||||
[CipherType.SshKey]: "newItemHeaderSshKey",
|
||||
},
|
||||
edit: {
|
||||
[CipherType.Login]: "editItemHeaderLogin",
|
||||
[CipherType.Card]: "editItemHeaderCard",
|
||||
[CipherType.Identity]: "editItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "editItemHeaderNote",
|
||||
[CipherType.SshKey]: "editItemHeaderSshKey",
|
||||
},
|
||||
};
|
||||
|
||||
const type = this.cipher?.type ?? this.formConfig.cipherType;
|
||||
let titleMode: "view" | "edit" | "new" = "view";
|
||||
|
||||
if (this.mode === "form") {
|
||||
titleMode =
|
||||
this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit" ? "edit" : "new";
|
||||
}
|
||||
|
||||
const fullTranslation = translation[titleMode][type];
|
||||
this.title = this.i18nService.t(fullTranslation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the vault item drawer.
|
||||
* @param dialogService Instance of the DialogService.
|
||||
* @param params The parameters for the drawer.
|
||||
* @returns The drawer result.
|
||||
*/
|
||||
static openDrawer(dialogService: DialogService, params: VaultItemDrawerParams) {
|
||||
return dialogService.openDrawer<VaultItemDrawerResult, VaultItemDrawerParams>(
|
||||
VaultItemDrawerComponent,
|
||||
{
|
||||
data: params,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,11 @@
|
||||
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
|
||||
<div id="vault" class="vault vault-v3" attr.aria-hidden="{{ showingModal }}">
|
||||
<app-vault-items-v2
|
||||
id="items"
|
||||
class="items"
|
||||
[activeCipherId]="cipherId"
|
||||
(onCipherClicked)="viewCipher($event)"
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
>
|
||||
</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)"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
></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"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="openAttachmentsDialog()"
|
||||
[disabled]="formDisabled"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<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>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
// import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -65,8 +65,8 @@ import {
|
||||
CipherFormConfigService,
|
||||
CipherFormGenerationService,
|
||||
CipherFormMode,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
// CipherFormModule,
|
||||
// CipherViewComponent,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
@@ -84,9 +84,10 @@ import { DesktopCredentialGenerationService } from "../../../services/desktop-ci
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { AssignCollectionsDesktopComponent } from "../vault/assign-collections";
|
||||
import { ItemFooterComponent } from "../vault/item-footer.component";
|
||||
import { VaultItemsV2Component } from "../vault/vault-items-v2.component";
|
||||
|
||||
import { VaultItemDrawerComponent, VaultItemDrawerResult } from "./vault-item-drawer.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -94,18 +95,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
@Component({
|
||||
selector: "app-vault-v3",
|
||||
templateUrl: "vault.component.html",
|
||||
imports: [
|
||||
BadgeModule,
|
||||
CommonModule,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
ItemFooterComponent,
|
||||
I18nPipe,
|
||||
ItemModule,
|
||||
ButtonModule,
|
||||
PremiumBadgeComponent,
|
||||
VaultItemsV2Component,
|
||||
],
|
||||
imports: [BadgeModule, CommonModule, I18nPipe, ItemModule, ButtonModule, VaultItemsV2Component],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherFormConfigService,
|
||||
@@ -286,20 +276,20 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
}
|
||||
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((): any => null);
|
||||
if (value) {
|
||||
this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
}
|
||||
}
|
||||
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((): any => null);
|
||||
// if (value) {
|
||||
// this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
default:
|
||||
detectChanges = false;
|
||||
break;
|
||||
@@ -416,19 +406,40 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
if (await this.shouldReprompt(cipher, "view")) {
|
||||
return;
|
||||
}
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
this.collections =
|
||||
this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null;
|
||||
this.action = "view";
|
||||
|
||||
await this.go().catch(() => {});
|
||||
// Build config for the drawer
|
||||
const config = await this.formConfigService
|
||||
.buildConfig("edit", cipher.id as CipherId, undefined)
|
||||
.catch((): any => null);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open drawer in view mode
|
||||
const drawerRef = VaultItemDrawerComponent.openDrawer(this.dialogService, {
|
||||
config,
|
||||
initialMode: "view",
|
||||
});
|
||||
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientViewed,
|
||||
cipher.id,
|
||||
false,
|
||||
cipher.organizationId,
|
||||
);
|
||||
|
||||
const result = await lastValueFrom(drawerRef.closed);
|
||||
|
||||
// Refresh list if cipher was modified
|
||||
if (result?.result === VaultItemDrawerResult.Saved) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
} else if (
|
||||
result?.result === VaultItemDrawerResult.Deleted ||
|
||||
result?.result === VaultItemDrawerResult.Restored
|
||||
) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
formStatusChanged(status: "disabled" | "enabled") {
|
||||
@@ -574,64 +585,64 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
) {
|
||||
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((): any => null);
|
||||
if (value) {
|
||||
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// 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((): any => 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(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
@@ -653,25 +664,62 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
if (await this.shouldReprompt(cipher, "edit")) {
|
||||
return;
|
||||
}
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
await this.buildFormConfig("edit");
|
||||
if (!cipher.edit && this.config) {
|
||||
this.config.mode = "partial-edit";
|
||||
|
||||
// Build config for the drawer
|
||||
const config = await this.formConfigService
|
||||
.buildConfig("edit", cipher.id as CipherId, undefined)
|
||||
.catch((): any => null);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cipher.edit) {
|
||||
config.mode = "partial-edit";
|
||||
}
|
||||
|
||||
// Open drawer in edit mode
|
||||
const drawerRef = VaultItemDrawerComponent.openDrawer(this.dialogService, {
|
||||
config,
|
||||
initialMode: "edit",
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(drawerRef.closed);
|
||||
|
||||
// Refresh list if cipher was modified
|
||||
if (result?.result === VaultItemDrawerResult.Saved) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
} else if (result?.result === VaultItemDrawerResult.Deleted) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
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(() => {});
|
||||
|
||||
// Build config for the drawer
|
||||
const config = await this.formConfigService
|
||||
.buildConfig("clone", cipher.id as CipherId, undefined)
|
||||
.catch((): any => null);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open drawer in clone mode
|
||||
const drawerRef = VaultItemDrawerComponent.openDrawer(this.dialogService, {
|
||||
config,
|
||||
initialMode: "clone",
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(drawerRef.closed);
|
||||
|
||||
// Refresh list if cipher was modified
|
||||
if (result?.result === VaultItemDrawerResult.Saved) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
@@ -713,16 +761,25 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
}
|
||||
|
||||
async addCipher(type: CipherType) {
|
||||
if (this.action === "add") {
|
||||
this.addType = type || this.activeFilter.cipherType;
|
||||
|
||||
// Build config for the drawer
|
||||
const config = await this.formConfigService
|
||||
.buildConfig("add", undefined, this.addType)
|
||||
.catch((): any => null);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
this.addType = type || this.activeFilter.cipherType;
|
||||
this.cipher = new CipherView();
|
||||
this.cipherId = null;
|
||||
await this.buildFormConfig("add");
|
||||
this.action = "add";
|
||||
this.prefillCipherFromFilter();
|
||||
await this.go().catch(() => {});
|
||||
|
||||
// Prefill cipher from filter
|
||||
this.prefillCipherConfig(config);
|
||||
|
||||
// Open drawer in add mode
|
||||
const drawerRef = VaultItemDrawerComponent.openDrawer(this.dialogService, {
|
||||
config,
|
||||
initialMode: "add",
|
||||
});
|
||||
|
||||
if (type === CipherType.SshKey) {
|
||||
this.toastService.showToast({
|
||||
@@ -731,6 +788,13 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
message: this.i18nService.t("sshKeyGenerated"),
|
||||
});
|
||||
}
|
||||
|
||||
const result = await lastValueFrom(drawerRef.closed);
|
||||
|
||||
// Refresh list if cipher was created
|
||||
if (result?.result === VaultItemDrawerResult.Saved) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async savedCipher(cipher: CipherView) {
|
||||
@@ -798,6 +862,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
};
|
||||
return filterFn(proxyCipher as any);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -971,12 +1036,48 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
|
||||
this.config.initialValues = {
|
||||
...this.config.initialValues,
|
||||
folderId: this.folderId,
|
||||
folderId: this.folderId || undefined,
|
||||
organizationId: this.addOrganizationId as OrganizationId,
|
||||
collectionIds: this.addCollectionIds as CollectionId[],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill cipher config based on active filter selections
|
||||
*/
|
||||
private prefillCipherConfig(config: CipherFormConfig) {
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let addOrganizationId: string | null = null;
|
||||
let addCollectionIds: string[] | null = null;
|
||||
let folderId: string | null | undefined = null;
|
||||
|
||||
if (this.activeFilter.collectionId != null) {
|
||||
const collections = this.filteredCollections?.filter(
|
||||
(c) => c.id === this.activeFilter.collectionId,
|
||||
);
|
||||
if (collections?.length > 0) {
|
||||
addOrganizationId = collections[0].organizationId;
|
||||
addCollectionIds = [this.activeFilter.collectionId];
|
||||
}
|
||||
} else if (this.activeFilter.organizationId) {
|
||||
addOrganizationId = this.activeFilter.organizationId;
|
||||
}
|
||||
|
||||
if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) {
|
||||
folderId = this.activeFilter.folderId;
|
||||
}
|
||||
|
||||
config.initialValues = {
|
||||
...config.initialValues,
|
||||
organizationId: addOrganizationId as OrganizationId,
|
||||
folderId: folderId || undefined,
|
||||
collectionIds: addCollectionIds as CollectionId[],
|
||||
};
|
||||
}
|
||||
|
||||
private async canNavigateAway(action: string, cipher?: CipherView) {
|
||||
if (this.action === action && (!cipher || this.cipherId === cipher.id)) {
|
||||
return false;
|
||||
|
||||
@@ -1,75 +1,88 @@
|
||||
<div class="footer">
|
||||
<ng-container *ngIf="!cipher.decryptionFailure">
|
||||
<button
|
||||
#submitBtn
|
||||
form="cipherForm"
|
||||
type="submit"
|
||||
[hidden]="action === 'view'"
|
||||
bitButton
|
||||
class="primary"
|
||||
appA11yTitle="{{ submitButtonText() }}"
|
||||
>
|
||||
{{ submitButtonText() }}
|
||||
</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()"
|
||||
>
|
||||
<button
|
||||
#submitBtn
|
||||
form="cipherForm"
|
||||
type="submit"
|
||||
[hidden]="action === 'view'"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
appA11yTitle="{{ submitButtonText() }}"
|
||||
>
|
||||
{{ submitButtonText() }}
|
||||
</button>
|
||||
@if (action === "edit" || action === "clone" || action === "add") {
|
||||
<button type="button" bitButton buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted && cipher.permissions.restore"
|
||||
>
|
||||
<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 === 'view'"
|
||||
(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="hasFooterAction">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="showArchiveButton"
|
||||
(click)="archive()"
|
||||
appA11yTitle="{{ 'archiveVerb' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-archive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="showUnarchiveButton"
|
||||
(click)="unarchive()"
|
||||
appA11yTitle="{{ 'unArchive' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-unarchive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<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>
|
||||
}
|
||||
@if (cipher && !cipher.decryptionFailure) {
|
||||
@if (!cipher.isDeleted && action === "view") {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
@if (cipher.isDeleted && cipher.permissions.restore) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
@if (cipher.id && !cipher?.organizationId && !cipher.isDeleted && action === "view") {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (cipher && hasFooterAction) {
|
||||
<div class="right">
|
||||
@if (showArchiveButton) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="archive()"
|
||||
appA11yTitle="{{ 'archiveVerb' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-archive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
@if (showUnarchiveButton) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="unarchive()"
|
||||
appA11yTitle="{{ 'unArchive' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-unarchive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="danger"
|
||||
(click)="delete()"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
input,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
@@ -36,7 +37,7 @@ import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitward
|
||||
export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) cipher: CipherView = new CipherView();
|
||||
@Input({ required: true }) cipher: CipherView | null = new CipherView();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() collectionId: string | null = null;
|
||||
@@ -68,6 +69,17 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
|
||||
|
||||
protected cipherService = inject(CipherService);
|
||||
protected dialogService = inject(DialogService);
|
||||
protected passwordRepromptService = inject(PasswordRepromptService);
|
||||
protected cipherAuthorizationService = inject(CipherAuthorizationService);
|
||||
protected accountService = inject(AccountService);
|
||||
protected toastService = inject(ToastService);
|
||||
protected i18nService = inject(I18nService);
|
||||
protected logService = inject(LogService);
|
||||
protected cipherArchiveService = inject(CipherArchiveService);
|
||||
protected archiveCipherUtilitiesService = inject(ArchiveCipherUtilitiesService);
|
||||
|
||||
readonly submitButtonText = input<string>(this.i18nService.t("save"));
|
||||
|
||||
activeUserId: UserId | null = null;
|
||||
@@ -76,19 +88,6 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
protected showArchiveButton = false;
|
||||
protected showUnarchiveButton = 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,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
protected archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
@@ -130,7 +129,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
return (
|
||||
this.showArchiveButton ||
|
||||
this.showUnarchiveButton ||
|
||||
(this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view"))
|
||||
(this.cipher?.permissions?.delete && (this.action === "edit" || this.action === "view"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,6 +220,12 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private async checkArchiveState() {
|
||||
if (!this.cipher) {
|
||||
this.showArchiveButton = false;
|
||||
this.showUnarchiveButton = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const cipherCanBeArchived = !this.cipher.isDeleted;
|
||||
const [userCanArchive, hasArchiveFlagEnabled] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
|
||||
@@ -176,6 +176,9 @@ export function getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (flag === FeatureFlag.DesktopUiMigrationMilestone1) {
|
||||
return true;
|
||||
}
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user