1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

[PM-7161] browser v2 view container (#9723)

* Build new view-v2 component and reusable view sections. Custom Fields, Item Details, Attachments, Additional Info,  Item History
This commit is contained in:
Jason Ng
2024-07-10 00:11:51 -04:00
committed by GitHub
parent 7dfef8991c
commit 6d6785297b
26 changed files with 846 additions and 8 deletions

View File

@@ -1483,6 +1483,15 @@
}
}
},
"viewItemHeader": {
"message": "View $TYPE$",
"placeholders": {
"type": {
"content": "$1",
"example": "Login"
}
}
},
"passwordHistory": {
"message": "Password history"
},
@@ -3535,6 +3544,30 @@
"contactYourOrgAdmin": {
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
},
"itemDetails": {
"message": "Item details"
},
"itemName": {
"message": "Item name"
},
"additionalInformation": {
"message": "Additional information"
},
"itemHistory": {
"message": "Item history"
},
"lastEdited": {
"message": "Last edited"
},
"ownerYou":{
"message": "Owner: You"
},
"linked": {
"message": "Linked"
},
"copySuccessful": {
"message": "Copy Successful"
},
"upload": {
"message": "Upload"
},
@@ -3574,6 +3607,15 @@
"filters": {
"message": "Filters"
},
"downloadAttachment": {
"message": "Download - $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
"example": "Your File"
}
}
},
"cardDetails": {
"message": "Card details"
},

View File

@@ -1,9 +1,12 @@
<footer
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
>
<div class="tw-max-w-screen-sm tw-mx-auto">
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-flex tw-justify-start tw-gap-2">
<ng-content></ng-content>
</div>
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
<ng-content select="[slot=end]"></ng-content>
</div>
</div>
</footer>

View File

@@ -266,6 +266,7 @@ class MockSettingsPageComponent {}
<popup-footer slot="footer">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
<button slot="end" type="button" buttonType="danger" bitIconButton="bwi-trash"></button>
</popup-footer>
</popup-page>
`,
@@ -279,6 +280,7 @@ class MockSettingsPageComponent {}
MockPopoutButtonComponent,
MockCurrentAccountComponent,
VaultComponent,
IconButtonModule,
],
})
class MockVaultSubpageComponent {}

View File

@@ -71,6 +71,7 @@ import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.compo
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
@@ -211,12 +212,11 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { state: "ciphers" },
},
{
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
path: "view-cipher",
component: ViewComponent,
canActivate: [AuthGuard],
data: { state: "view-cipher" },
},
}),
{
path: "cipher-password-history",
component: PasswordHistoryComponent,

View File

@@ -21,7 +21,7 @@
<a
bit-item-content
[routerLink]="['/view-cipher']"
[queryParams]="{ cipherId: cipher.id }"
[queryParams]="{ cipherId: cipher.id, type: cipher.type }"
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
>
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>

View File

@@ -0,0 +1,20 @@
<popup-page>
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
<popup-footer slot="footer">
<a bitButton type="button" buttonType="primary" (click)="editCipher()">
{{ "edit" | i18n }}
</a>
<button
slot="end"
*ngIf="cipher && cipher.edit"
[bitAction]="delete"
type="button"
buttonType="danger"
bitIconButton="bwi-trash"
[appA11yTitle]="'delete' | i18n"
></button>
</popup-footer>
</popup-page>

View File

@@ -0,0 +1,157 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AsyncActionsModule,
SearchModule,
ButtonModule,
IconButtonModule,
DialogService,
ToastService,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
@Component({
selector: "app-view-v2",
templateUrl: "view-v2.component.html",
standalone: true,
imports: [
CommonModule,
SearchModule,
JslibModule,
FormsModule,
ButtonModule,
PopupPageComponent,
PopupHeaderComponent,
PopupFooterComponent,
IconButtonModule,
CipherViewComponent,
AsyncActionsModule,
],
})
export class ViewV2Component {
headerText: string;
cipherId: string;
cipher: CipherView;
organization$: Observable<Organization>;
folder$: Observable<FolderView>;
collections$: Observable<CollectionView[]>;
private passwordReprompted = false;
constructor(
private route: ActivatedRoute,
private router: Router,
private i18nService: I18nService,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService,
private dialogService: DialogService,
private logService: LogService,
private toastService: ToastService,
) {
this.subscribeToParams();
}
subscribeToParams(): void {
this.route.queryParams
.pipe(
switchMap((param) => {
return this.getCipherData(param.cipherId);
}),
takeUntilDestroyed(),
)
.subscribe((data) => {
this.cipher = data;
this.headerText = this.setHeader(data.type);
});
}
setHeader(type: CipherType) {
switch (type) {
case CipherType.Login:
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin"));
case CipherType.Card:
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard"));
case CipherType.Identity:
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity"));
case CipherType.SecureNote:
return this.i18nService.t("viewItemHeader", this.i18nService.t("note"));
}
}
async getCipherData(id: string) {
const cipher = await this.cipherService.get(id);
return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
}
editCipher() {
if (this.cipher.isDeleted) {
return false;
}
void this.router.navigate(["/edit-cipher"], {
queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false },
});
return true;
}
delete = async (): Promise<boolean> => {
this.passwordReprompted =
this.passwordReprompted ||
(await this.passwordRepromptService.passwordRepromptCheck(this.cipher));
if (!this.passwordReprompted) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: {
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
},
type: "warning",
});
if (!confirmed) {
return false;
}
try {
await this.deleteCipher();
} catch (e) {
this.logService.error(e);
return false;
}
await this.router.navigate(["/vault"]);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"),
});
return true;
};
protected deleteCipher() {
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id)
: this.cipherService.softDeleteWithServer(this.cipher.id);
}
}

View File

@@ -198,7 +198,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
super.selectCipher(cipher);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id },
});
}
this.preventSelected = false;
}, 200);