1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

extended existing item-footer component

This commit is contained in:
Leslie Xiong
2026-01-27 11:58:48 -05:00
parent 4cbb2077e3
commit e198edaeec

View File

@@ -1,36 +1,10 @@
import { CommonModule } from "@angular/common";
import {
Input,
Output,
EventEmitter,
Component,
OnInit,
ViewChild,
OnChanges,
SimpleChanges,
input,
} from "@angular/core";
import { combineLatest, firstValueFrom, switchMap } from "rxjs";
import { Component } from "@angular/core";
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 { UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
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,
IconButtonModule,
DialogService,
ToastService,
} from "@bitwarden/components";
import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitwarden/vault";
import { ButtonModule, IconButtonModule } from "@bitwarden/components";
import { ItemFooterComponent as BaseItemFooterComponent } from "../../vault/item-footer.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -39,166 +13,14 @@ import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitward
templateUrl: "item-footer.component.html",
imports: [ButtonModule, IconButtonModule, CommonModule, JslibModule],
})
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();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() collectionId: string | null = null;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ required: true }) action: string = "view";
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() masterPasswordAlreadyPrompted: boolean = false;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onEdit = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onClone = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onDelete = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onRestore = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onCancel = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onArchiveToggle = new EventEmitter<CipherView>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
readonly submitButtonText = input<string>(this.i18nService.t("save"));
activeUserId: UserId | null = null;
passwordReprompted: boolean = false;
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;
await this.checkArchiveState();
}
async ngOnChanges(changes: SimpleChanges) {
if (changes.cipher) {
await this.checkArchiveState();
}
}
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);
}
protected get hasFooterAction() {
return (
this.showArchiveButton ||
this.showUnarchiveButton ||
(this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view"))
);
}
cancel() {
this.onCancel.emit(this.cipher);
}
async delete() {
export class ItemFooterComponent extends BaseItemFooterComponent {
async delete(): Promise<boolean> {
this.onDelete.emit(this.cipher);
return true;
}
async restore() {
async restore(): Promise<boolean> {
this.onRestore.emit(this.cipher);
}
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());
}
protected async archive() {
await this.archiveCipherUtilitiesService.archiveCipher(this.cipher);
this.onArchiveToggle.emit();
}
protected async unarchive() {
await this.archiveCipherUtilitiesService.unarchiveCipher(this.cipher);
this.onArchiveToggle.emit();
}
private async checkArchiveState() {
const cipherCanBeArchived = !this.cipher.isDeleted;
const [userCanArchive, hasArchiveFlagEnabled] = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((id) =>
combineLatest([
this.cipherArchiveService.userCanArchive$(id),
this.cipherArchiveService.hasArchiveFlagEnabled$,
]),
),
),
);
this.showArchiveButton =
cipherCanBeArchived && userCanArchive && this.action === "view" && !this.cipher.isArchived;
// A user should always be able to unarchive an archived item
this.showUnarchiveButton =
hasArchiveFlagEnabled && this.action === "view" && this.cipher.isArchived;
return true;
}
}