mirror of
https://github.com/bitwarden/browser
synced 2026-01-30 16:23:53 +00:00
migrated vault cipher list
This commit is contained in:
@@ -26,7 +26,6 @@ import { UserVerificationComponent } from "./components/user-verification.compon
|
||||
import { AccountSwitcherComponent } from "./layout/account-switcher.component";
|
||||
import { HeaderComponent } from "./layout/header.component";
|
||||
import { NavComponent } from "./layout/nav.component";
|
||||
import { SearchComponent } from "./layout/search/search.component";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
@@ -51,7 +50,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
ColorPasswordCountPipe,
|
||||
HeaderComponent,
|
||||
PremiumComponent,
|
||||
SearchComponent,
|
||||
],
|
||||
providers: [
|
||||
SshAgentService,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-layout class="!tw-h-full" rounded>
|
||||
<bit-layout class="!tw-h-full">
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n" />
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<div class="header">
|
||||
<app-search></app-search>
|
||||
<app-account-switcher></app-account-switcher>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
<div class="search" *ngIf="state.enabled">
|
||||
<input
|
||||
type="search"
|
||||
[placeholder]="state.placeholderText"
|
||||
id="search"
|
||||
autocomplete="off"
|
||||
[formControl]="searchText"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
@if (state.enabled) {
|
||||
<bit-search [placeholder]="state.placeholderText" [formControl]="searchText" appAutofocus>
|
||||
</bit-search>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { UntypedFormControl } from "@angular/forms";
|
||||
import { ReactiveFormsModule, UntypedFormControl } from "@angular/forms";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofocusDirective, SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { SearchBarService, SearchBarState } from "./search-bar.service";
|
||||
|
||||
@@ -13,7 +15,7 @@ import { SearchBarService, SearchBarState } from "./search-bar.service";
|
||||
@Component({
|
||||
selector: "app-search",
|
||||
templateUrl: "search.component.html",
|
||||
standalone: false,
|
||||
imports: [CommonModule, ReactiveFormsModule, AutofocusDirective, SearchModule],
|
||||
})
|
||||
export class SearchComponent implements OnInit, OnDestroy {
|
||||
state: SearchBarState;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"favorites": {
|
||||
"message": "Favorites"
|
||||
},
|
||||
"unfavorite": {
|
||||
"message": "Unfavorite"
|
||||
},
|
||||
"types": {
|
||||
"message": "Types"
|
||||
},
|
||||
@@ -47,6 +50,21 @@
|
||||
"addItem": {
|
||||
"message": "Add item"
|
||||
},
|
||||
"addLogin": {
|
||||
"message": "Add login"
|
||||
},
|
||||
"addCard": {
|
||||
"message": "Add card"
|
||||
},
|
||||
"addIdentity": {
|
||||
"message": "Add identity"
|
||||
},
|
||||
"addSecureNote": {
|
||||
"message": "Add secure note"
|
||||
},
|
||||
"addSshKey": {
|
||||
"message": "Add SSH key"
|
||||
},
|
||||
"shared": {
|
||||
"message": "Shared"
|
||||
},
|
||||
@@ -84,6 +102,21 @@
|
||||
"viewItem": {
|
||||
"message": "View item"
|
||||
},
|
||||
"viewLogin": {
|
||||
"message": "View login"
|
||||
},
|
||||
"viewCard": {
|
||||
"message": "View card"
|
||||
},
|
||||
"viewIdentity": {
|
||||
"message": "View identity"
|
||||
},
|
||||
"viewSecureNote": {
|
||||
"message": "View secure note"
|
||||
},
|
||||
"viewSshKey": {
|
||||
"message": "View SSH key"
|
||||
},
|
||||
"name": {
|
||||
"message": "Name"
|
||||
},
|
||||
@@ -100,70 +133,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"deletionDateDescV2": {
|
||||
"message": "The Send will be permanently deleted on this date.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"fileToShare": {
|
||||
"message": "File to share"
|
||||
},
|
||||
"hideTextByDefault": {
|
||||
"message": "Hide text by default"
|
||||
},
|
||||
"hideYourEmail": {
|
||||
"message": "Hide your email address from viewers."
|
||||
},
|
||||
"limitSendViews": {
|
||||
"message": "Limit views"
|
||||
},
|
||||
"limitSendViewsCount": {
|
||||
"message": "$ACCESSCOUNT$ views left",
|
||||
"description": "Displayed under the limit views field on Send",
|
||||
"placeholders": {
|
||||
"accessCount": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"limitSendViewsHint": {
|
||||
"message": "No one can view this Send after the limit is reached.",
|
||||
"description": "Displayed under the limit views field on Send"
|
||||
},
|
||||
"privateNote": {
|
||||
"message": "Private note"
|
||||
},
|
||||
"sendDetails": {
|
||||
"message": "Send details",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendTypeTextToShare": {
|
||||
"message": "Text to share"
|
||||
},
|
||||
"newItemHeaderTextSend": {
|
||||
"message": "New Text Send",
|
||||
"description": "Header for new text send"
|
||||
},
|
||||
"newItemHeaderFileSend": {
|
||||
"message": "New File Send",
|
||||
"description": "Header for new file send"
|
||||
},
|
||||
"editItemHeaderTextSend": {
|
||||
"message": "Edit Text Send",
|
||||
"description": "Header for edit text send"
|
||||
},
|
||||
"editItemHeaderFileSend": {
|
||||
"message": "Edit File Send",
|
||||
"description": "Header for edit file send"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"new": {
|
||||
"message": "New",
|
||||
"description": "for adding new items"
|
||||
@@ -183,6 +152,21 @@
|
||||
"editItem": {
|
||||
"message": "Edit item"
|
||||
},
|
||||
"editLogin": {
|
||||
"message": "Edit login"
|
||||
},
|
||||
"editCard": {
|
||||
"message": "Edit card"
|
||||
},
|
||||
"editIdentity": {
|
||||
"message": "Edit identity"
|
||||
},
|
||||
"editSecureNote": {
|
||||
"message": "Edit secure note"
|
||||
},
|
||||
"editSshKey": {
|
||||
"message": "Edit SSH key"
|
||||
},
|
||||
"emailAddress": {
|
||||
"message": "Email address"
|
||||
},
|
||||
@@ -590,6 +574,12 @@
|
||||
"editedItem": {
|
||||
"message": "Item saved"
|
||||
},
|
||||
"itemAddedToFavorites": {
|
||||
"message": "Item added to favorites"
|
||||
},
|
||||
"itemRemovedFromFavorites": {
|
||||
"message": "Item removed from favorites"
|
||||
},
|
||||
"deleteItem": {
|
||||
"message": "Delete item"
|
||||
},
|
||||
@@ -605,6 +595,15 @@
|
||||
"deletedItem": {
|
||||
"message": "Item sent to trash"
|
||||
},
|
||||
"deletedCollectionId": {
|
||||
"message": "Deleted collection $ID$.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
"example": "Server Passwords"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overwritePasswordConfirmation": {
|
||||
"message": "Are you sure you want to overwrite the current password?"
|
||||
},
|
||||
@@ -815,6 +814,15 @@
|
||||
"deletedFolder": {
|
||||
"message": "Folder deleted"
|
||||
},
|
||||
"editInfo": {
|
||||
"message": "Edit info"
|
||||
},
|
||||
"access": {
|
||||
"message": "Access"
|
||||
},
|
||||
"editAccess": {
|
||||
"message": "Edit access"
|
||||
},
|
||||
"loginOrCreateNewAccount": {
|
||||
"message": "Log in or create a new account to access your secure vault."
|
||||
},
|
||||
@@ -1556,6 +1564,21 @@
|
||||
"unknown": {
|
||||
"message": "Unknown"
|
||||
},
|
||||
"unknownCipher": {
|
||||
"message": "Unknown item, you may need to request permission to access this item."
|
||||
},
|
||||
"copyAddress": {
|
||||
"message": "Copy address"
|
||||
},
|
||||
"copyPhone": {
|
||||
"message": "Copy phone"
|
||||
},
|
||||
"copyNote": {
|
||||
"message": "Copy note"
|
||||
},
|
||||
"copyVerificationCode": {
|
||||
"message": "Copy verification code"
|
||||
},
|
||||
"copyUsername": {
|
||||
"message": "Copy username"
|
||||
},
|
||||
@@ -2057,6 +2080,24 @@
|
||||
"clone": {
|
||||
"message": "Clone"
|
||||
},
|
||||
"cloneItem": {
|
||||
"message": "Clone item"
|
||||
},
|
||||
"cloneLogin": {
|
||||
"message": "Clone login"
|
||||
},
|
||||
"cloneCard": {
|
||||
"message": "Clone card"
|
||||
},
|
||||
"cloneIdentity": {
|
||||
"message": "Clone identity"
|
||||
},
|
||||
"cloneSecureNote": {
|
||||
"message": "Clone secure note"
|
||||
},
|
||||
"cloneSshKey": {
|
||||
"message": "Clone SSH key"
|
||||
},
|
||||
"passwordGeneratorPolicyInEffect": {
|
||||
"message": "One or more organization policies are affecting your generator settings."
|
||||
},
|
||||
@@ -2080,9 +2121,33 @@
|
||||
"message": "Trash",
|
||||
"description": "Noun: a special folder to hold deleted items"
|
||||
},
|
||||
"trashCleanupWarning": {
|
||||
"message": "Items that have been in trash more than 30 days will be automatically deleted."
|
||||
},
|
||||
"trashCleanupWarningSelfHosted": {
|
||||
"message": "Items that have been in trash for a while will be automatically deleted."
|
||||
},
|
||||
"searchTrash": {
|
||||
"message": "Search trash"
|
||||
},
|
||||
"searchArchive": {
|
||||
"message": "Search archive"
|
||||
},
|
||||
"searchLogin": {
|
||||
"message": "Search login"
|
||||
},
|
||||
"searchCard": {
|
||||
"message": "Search card"
|
||||
},
|
||||
"searchIdentity": {
|
||||
"message": "Search identity"
|
||||
},
|
||||
"searchSecureNote": {
|
||||
"message": "Search secure note"
|
||||
},
|
||||
"searchSshKey": {
|
||||
"message": "Search SSH key"
|
||||
},
|
||||
"permanentlyDeleteItem": {
|
||||
"message": "Permanently delete item"
|
||||
},
|
||||
@@ -2092,9 +2157,6 @@
|
||||
"permanentlyDeletedItem": {
|
||||
"message": "Item permanently deleted"
|
||||
},
|
||||
"archivedItemRestored": {
|
||||
"message": "Archived item restored"
|
||||
},
|
||||
"restoredItem": {
|
||||
"message": "Item restored"
|
||||
},
|
||||
@@ -2379,6 +2441,9 @@
|
||||
"message": "Edit Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"me": {
|
||||
"message": "Me"
|
||||
},
|
||||
"myVault": {
|
||||
"message": "My vault"
|
||||
},
|
||||
@@ -3780,6 +3845,93 @@
|
||||
"collection": {
|
||||
"message": "Collection"
|
||||
},
|
||||
"editCollection": {
|
||||
"message": "Edit collection"
|
||||
},
|
||||
"nestCollectionUnder": {
|
||||
"message": "Nest collection under"
|
||||
},
|
||||
"collectionInfo": {
|
||||
"message": "Collection info"
|
||||
},
|
||||
"grantCollectionAccess": {
|
||||
"message": "Grant groups or members access to this collection."
|
||||
},
|
||||
"permission": {
|
||||
"message": "Permission"
|
||||
},
|
||||
"viewItems": {
|
||||
"message": "View items"
|
||||
},
|
||||
"viewItemsHidePass": {
|
||||
"message": "View items, hidden passwords"
|
||||
},
|
||||
"editItems": {
|
||||
"message": "Edit items"
|
||||
},
|
||||
"editItemsHidePass": {
|
||||
"message": "Edit items, hidden passwords"
|
||||
},
|
||||
"manageCollection": {
|
||||
"message": "Manage collection"
|
||||
},
|
||||
"selectGroupsAndMembers": {
|
||||
"message": "Select groups and members"
|
||||
},
|
||||
"newCollection": {
|
||||
"message": "New collection"
|
||||
},
|
||||
"externalId": {
|
||||
"message": "External ID"
|
||||
},
|
||||
"externalIdDesc": {
|
||||
"message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API."
|
||||
},
|
||||
"noCollection": {
|
||||
"message": "No collection"
|
||||
},
|
||||
"deleted": {
|
||||
"message": "Deleted"
|
||||
},
|
||||
"readOnlyCollectionAccess": {
|
||||
"message": "You do not have access to manage this collection."
|
||||
},
|
||||
"grantManageCollectionWarningTitle": {
|
||||
"message": "Missing Manage Collection Permissions"
|
||||
},
|
||||
"grantManageCollectionWarning": {
|
||||
"message": "Grant Manage collection permissions to allow full collection management including deletion of collection."
|
||||
},
|
||||
"grantCollectionAccessMembersOnly": {
|
||||
"message": "Grant members access to this collection."
|
||||
},
|
||||
"adminCollectionAccess": {
|
||||
"message": "Administrators can access and manage collections."
|
||||
},
|
||||
"managePermissionRequired": {
|
||||
"message": "At least one member or group must have can manage permission."
|
||||
},
|
||||
"userPermissionOverrideHelperDesc": {
|
||||
"message": "Permissions set for a member will replace permissions set by that member's group."
|
||||
},
|
||||
"noMembersOrGroupsAdded": {
|
||||
"message": "No members or groups added"
|
||||
},
|
||||
"memberColumnHeader": {
|
||||
"message": "Member"
|
||||
},
|
||||
"selectMembers": {
|
||||
"message": "Select members"
|
||||
},
|
||||
"noMembersAdded": {
|
||||
"message": "No members added"
|
||||
},
|
||||
"groupSlashMemberColumnHeader": {
|
||||
"message": "Group/Member"
|
||||
},
|
||||
"deleteCollectionConfirmation": {
|
||||
"message": "Are you sure you want to delete this collection?"
|
||||
},
|
||||
"lastPassYubikeyDesc": {
|
||||
"message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button."
|
||||
},
|
||||
@@ -4087,6 +4239,9 @@
|
||||
"missingWebsite": {
|
||||
"message": "Missing website"
|
||||
},
|
||||
"missingPermissions": {
|
||||
"message": "You lack the necessary permissions to perform this action."
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
@@ -4400,8 +4555,8 @@
|
||||
"archiveItem": {
|
||||
"message": "Archive item"
|
||||
},
|
||||
"archiveItemDialogContent": {
|
||||
"message": "Once archived, this item will be excluded from search results and autofill suggestions."
|
||||
"archiveItemConfirmDesc": {
|
||||
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||
},
|
||||
"unArchiveAndSave": {
|
||||
"message": "Unarchive and save"
|
||||
@@ -4588,4 +4743,4 @@
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.btn,
|
||||
.vault .footer button,
|
||||
.modal-footer button {
|
||||
border-radius: $border-radius;
|
||||
padding: 7px 15px;
|
||||
|
||||
@@ -28,9 +28,8 @@ app-root {
|
||||
|
||||
> .items {
|
||||
order: 2;
|
||||
width: 28%;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
max-width: 350px;
|
||||
border-right: 1px solid #000000;
|
||||
|
||||
@include themify($themes) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-bg-bg-secondary tw-border-0 tw-border-t tw-border-solid tw-border-border-base tw-py-[20px] tw-pl-[24px] tw-pr-[12px]"
|
||||
>
|
||||
@if (!cipher.decryptionFailure) {
|
||||
<button
|
||||
#submitBtn
|
||||
form="cipherForm"
|
||||
type="submit"
|
||||
[hidden]="action === 'view'"
|
||||
bitButton
|
||||
class="primary"
|
||||
appA11yTitle="{{ submitButtonText() }}"
|
||||
>
|
||||
{{ submitButtonText() }}
|
||||
</button>
|
||||
@if (!cipher.isDeleted && action === "view") {
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
(click)="edit()"
|
||||
[appA11yTitle]="'edit' | i18n"
|
||||
>
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (cipher.isDeleted && cipher.permissions.restore) {
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (hasFooterAction) {
|
||||
<div class="tw-ml-auto tw-flex">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
[label]="(cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n"
|
||||
(click)="delete()"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,204 @@
|
||||
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 { 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";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-item-footer",
|
||||
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() {
|
||||
this.onDelete.emit(this.cipher);
|
||||
}
|
||||
|
||||
async restore() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<button
|
||||
bitBadge
|
||||
type="button"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background-color]="color"
|
||||
appA11yTitle="{{ organizationName }}"
|
||||
routerLink
|
||||
[queryParams]="{ organizationId: organizationIdLink }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
{{ name | ellipsis: 13 }}
|
||||
</button>
|
||||
@@ -0,0 +1,74 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Unassigned } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { BadgeModule } from "@bitwarden/components";
|
||||
import { OrganizationId } from "@bitwarden/sdk-internal";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-org-badge",
|
||||
templateUrl: "organization-name-badge.component.html",
|
||||
imports: [RouterModule, JslibModule, BadgeModule],
|
||||
})
|
||||
export class OrganizationNameBadgeComponent implements OnChanges {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() organizationId?: OrganizationId | string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() organizationName: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() disabled: boolean;
|
||||
|
||||
// Need a separate variable or we get weird behavior when used as part of cdk virtual scrolling
|
||||
name: string;
|
||||
color: string;
|
||||
textColor: string;
|
||||
isMe: boolean;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private avatarService: AvatarService,
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
// ngOnChanges is required since this component might be reused as part of
|
||||
// cdk virtual scrolling
|
||||
async ngOnChanges() {
|
||||
this.isMe = this.organizationName == null || this.organizationName === "";
|
||||
|
||||
if (this.isMe) {
|
||||
this.name = this.i18nService.t("me");
|
||||
this.color = await firstValueFrom(this.avatarService.avatarColor$);
|
||||
if (this.color == null) {
|
||||
const userId = await this.tokenService.getUserId();
|
||||
if (userId != null) {
|
||||
this.color = Utils.stringToColor(userId);
|
||||
} else {
|
||||
const userName =
|
||||
(await this.tokenService.getName()) ?? (await this.tokenService.getEmail());
|
||||
this.color = Utils.stringToColor(userName.toUpperCase());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.name = this.organizationName;
|
||||
this.color = Utils.stringToColor(this.organizationName.toUpperCase());
|
||||
}
|
||||
this.textColor = Utils.pickTextColorBasedOnBgColor(this.color, 135, true) + "!important";
|
||||
}
|
||||
|
||||
get organizationIdLink() {
|
||||
return this.organizationId ?? Unassigned;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { GetOrgNameFromIdPipe } from "@bitwarden/vault";
|
||||
|
||||
@NgModule({
|
||||
declarations: [GetOrgNameFromIdPipe],
|
||||
exports: [GetOrgNameFromIdPipe],
|
||||
})
|
||||
export class PipesModule {}
|
||||
@@ -0,0 +1,309 @@
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-truncate tw-flex tw-items-center tw-gap-2">
|
||||
<app-vault-icon [cipher]="cipher()"></app-vault-icon>
|
||||
<div class="tw-inline-flex tw-w-full">
|
||||
<button
|
||||
bitLink
|
||||
class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug"
|
||||
[disabled]="disabled()"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ itemId: cipher().id, action: clickAction() }"
|
||||
queryParamsHandling="merge"
|
||||
[replaceUrl]="true"
|
||||
title="{{ 'editItemWithName' | i18n: cipher().name }}"
|
||||
type="button"
|
||||
appStopProp
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{{ cipher().name }}
|
||||
</button>
|
||||
@if (hasAttachments()) {
|
||||
<i
|
||||
class="bwi bwi-paperclip tw-ml-2 tw-leading-normal"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
@if (showFixOldAttachments()) {
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle tw-ml-2 tw-leading-normal tw-text-warning"
|
||||
appStopProp
|
||||
title="{{ 'attachmentsNeedFix' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachmentsNeedFix" | i18n }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<br />
|
||||
<span class="tw-text-sm tw-text-muted" appStopProp>{{ subtitle() }}</span>
|
||||
</td>
|
||||
@if (showOwner()) {
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-hidden lg:tw-table-cell">
|
||||
<app-org-badge
|
||||
[disabled]="disabled()"
|
||||
[organizationId]="cipher().organizationId"
|
||||
[organizationName]="cipher().organizationId | orgNameFromId: organizations()"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
}
|
||||
@if (showGroups()) {
|
||||
<td bitCell [ngClass]="RowHeightClass"></td>
|
||||
}
|
||||
@if (viewingOrgVault()) {
|
||||
<td bitCell [ngClass]="RowHeightClass">
|
||||
<p class="tw-mb-0 tw-text-muted">
|
||||
{{ permissionText() }}
|
||||
</p>
|
||||
</td>
|
||||
}
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
|
||||
@if (decryptionFailure()) {
|
||||
<button
|
||||
[disabled]="disabled() || !canManageCollection()"
|
||||
[bitMenuTriggerFor]="corruptedCipherOptions"
|
||||
size="small"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
type="button"
|
||||
label="{{ 'options' | i18n }}"
|
||||
appStopProp
|
||||
></button>
|
||||
<bit-menu #corruptedCipherOptions>
|
||||
@if (canDeleteCipher()) {
|
||||
<button bitMenuItem (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (isDeleted() ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
} @else {
|
||||
@if (canLaunch()) {
|
||||
<a [href]="launchUri()" target="_blank" rel="noreferrer">
|
||||
<button
|
||||
[disabled]="disabled()"
|
||||
bitIconButton="bwi-external-link"
|
||||
type="button"
|
||||
appStopProp
|
||||
label="{{ 'launch' | i18n }}"
|
||||
></button>
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
[bitMenuTriggerFor]="copyOptions"
|
||||
[disabled]="disabled()"
|
||||
bitIconButton="bwi-clone"
|
||||
type="button"
|
||||
appStopProp
|
||||
label="{{ 'copyValue' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #copyOptions>
|
||||
@if (isLoginCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
@if (cipher().viewPassword) {
|
||||
<button bitMenuItem type="button" appCopyField="password" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button bitMenuItem type="button" appCopyField="totp" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyVerificationCode" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isCardCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNumber" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copySecurityCode" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isIdentityCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyEmail" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPhone" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyAddress" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isSecureNoteCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="secureNote" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNote" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
<button
|
||||
[bitMenuTriggerFor]="cipherOptions"
|
||||
[disabled]="disabled()"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
type="button"
|
||||
appStopProp
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #cipherOptions>
|
||||
@if (canLaunch()) {
|
||||
<a bitMenuItem type="button" [href]="launchUri()" target="_blank" rel="noreferrer">
|
||||
<i class="bwi bwi-fw bwi-external-link" aria-hidden="true"></i>
|
||||
{{ "launch" | i18n }}
|
||||
</a>
|
||||
}
|
||||
@if (isLoginCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
@if (cipher().viewPassword) {
|
||||
<button bitMenuItem type="button" appCopyField="password" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button bitMenuItem type="button" appCopyField="totp" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyVerificationCode" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isCardCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNumber" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copySecurityCode" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isIdentityCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyEmail" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPhone" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyAddress" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isSecureNoteCipher()) {
|
||||
<button type="button" bitMenuItem appCopyField="secureNote" [cipher]="cipher()">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyNote" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showMenuDivider()) {
|
||||
<bit-menu-divider />
|
||||
}
|
||||
@if (!viewingOrgVault()) {
|
||||
@if (showFavorite()) {
|
||||
<button bitMenuItem type="button" (click)="toggleFavorite()">
|
||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>
|
||||
{{ (cipher().favorite ? "unfavorite" : "favorite") | i18n }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (canEditCipher()) {
|
||||
<button bitMenuItem type="button" (click)="editCipher()">
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showAttachments()) {
|
||||
<button bitMenuItem type="button" (click)="attachments()">
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showClone()) {
|
||||
<button bitMenuItem type="button" (click)="clone()">
|
||||
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
|
||||
{{ "clone" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showAssignToCollections()) {
|
||||
<button bitMenuItem type="button" (click)="assignToCollections()">
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showEventLogs()) {
|
||||
<button bitMenuItem type="button" (click)="events()">
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showArchiveButton()) {
|
||||
@if (userCanArchive()) {
|
||||
<button bitMenuItem (click)="archive()" type="button">
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||
{{ "archiveVerb" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (!userCanArchive()) {
|
||||
<button bitMenuItem (click)="badge.promptForPremium($event)" type="button">
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||
{{ "archiveVerb" | i18n }}
|
||||
<!-- Hide app-premium badge from accessibility tools as it results in a button within a button -->
|
||||
<div slot="end" class="-tw-mt-0.5" aria-hidden>
|
||||
<app-premium-badge #badge></app-premium-badge>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showUnArchiveButton()) {
|
||||
<button bitMenuItem (click)="unarchive()" type="button">
|
||||
<i class="bwi bwi-fw bwi-unarchive" aria-hidden="true"></i>
|
||||
{{ "unArchive" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isDeleted() && canRestoreCipher()) {
|
||||
<button bitMenuItem (click)="restore()" type="button">
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (canDeleteCipher()) {
|
||||
<button bitMenuItem (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (isDeleted() ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
}
|
||||
</td>
|
||||
@@ -0,0 +1,308 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, HostListener, ViewChild, computed, input, output, inject } from "@angular/core";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuTriggerForDirective } from "@bitwarden/components";
|
||||
import { VaultItemEvent } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "tr[appVaultCipherRow]",
|
||||
templateUrl: "vault-cipher-row.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultCipherRowComponent<C extends CipherViewLike> {
|
||||
protected RowHeightClass = `tw-h-[75px]`;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective;
|
||||
|
||||
protected readonly disabled = input<boolean>();
|
||||
protected readonly cipher = input<C>();
|
||||
protected readonly showOwner = input<boolean>();
|
||||
protected readonly showGroups = input<boolean>();
|
||||
protected readonly showPremiumFeatures = input<boolean>();
|
||||
protected readonly useEvents = input<boolean>();
|
||||
protected readonly cloneable = input<boolean>();
|
||||
protected readonly organizations = input<Organization[]>();
|
||||
protected readonly viewingOrgVault = input<boolean>();
|
||||
protected readonly canEditCipher = input<boolean>();
|
||||
protected readonly canAssignCollections = input<boolean>();
|
||||
protected readonly canManageCollection = input<boolean>();
|
||||
/**
|
||||
* uses new permission delete logic from PM-15493
|
||||
*/
|
||||
protected readonly canDeleteCipher = input<boolean>();
|
||||
/**
|
||||
* uses new permission restore logic from PM-15493
|
||||
*/
|
||||
protected readonly canRestoreCipher = input<boolean>();
|
||||
/**
|
||||
* user has archive permissions
|
||||
*/
|
||||
protected readonly userCanArchive = input<boolean>();
|
||||
/** Archive feature is enabled */
|
||||
readonly archiveEnabled = input.required<boolean>();
|
||||
/**
|
||||
* Enforce Org Data Ownership Policy Status
|
||||
*/
|
||||
protected readonly enforceOrgDataOwnershipPolicy = input<boolean>();
|
||||
protected readonly onEvent = output<VaultItemEvent<C>>();
|
||||
|
||||
protected CipherType = CipherType;
|
||||
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
// Archive button will not show in Admin Console
|
||||
protected readonly showArchiveButton = computed(() => {
|
||||
if (!this.archiveEnabled() || this.viewingOrgVault()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!CipherViewLikeUtils.isArchived(this.cipher()) &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher())
|
||||
);
|
||||
});
|
||||
|
||||
// If item is archived always show unarchive button, even if user is not premium
|
||||
protected readonly showUnArchiveButton = computed(() => {
|
||||
if (!this.archiveEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CipherViewLikeUtils.isArchived(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly clickAction = computed(() => {
|
||||
if (this.decryptionFailure()) {
|
||||
return "showFailedToDecrypt";
|
||||
}
|
||||
|
||||
return "view";
|
||||
});
|
||||
|
||||
protected readonly showTotpCopyButton = computed(() => {
|
||||
const login = CipherViewLikeUtils.getLogin(this.cipher());
|
||||
|
||||
const hasTotp = login?.totp ?? false;
|
||||
|
||||
return hasTotp && (this.cipher().organizationUseTotp || this.showPremiumFeatures());
|
||||
});
|
||||
|
||||
protected readonly showFixOldAttachments = computed(() => {
|
||||
return this.cipher().hasOldAttachments && this.cipher().organizationId == null;
|
||||
});
|
||||
|
||||
protected readonly hasAttachments = computed(() => {
|
||||
return CipherViewLikeUtils.hasAttachments(this.cipher());
|
||||
});
|
||||
|
||||
// Do not show attachments button if:
|
||||
// item is archived AND user is not premium user
|
||||
protected readonly showAttachments = computed(() => {
|
||||
if (CipherViewLikeUtils.isArchived(this.cipher()) && !this.userCanArchive()) {
|
||||
return false;
|
||||
}
|
||||
return this.canEditCipher() || this.hasAttachments();
|
||||
});
|
||||
|
||||
protected readonly canLaunch = computed(() => {
|
||||
return CipherViewLikeUtils.canLaunch(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly launchUri = computed(() => {
|
||||
return CipherViewLikeUtils.getLaunchUri(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly subtitle = computed(() => {
|
||||
return CipherViewLikeUtils.subtitle(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly isDeleted = computed(() => {
|
||||
return CipherViewLikeUtils.isDeleted(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly decryptionFailure = computed(() => {
|
||||
return CipherViewLikeUtils.decryptionFailure(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly showFavorite = computed(() => {
|
||||
if (CipherViewLikeUtils.isArchived(this.cipher()) && !this.userCanArchive()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Do Not show Assign to Collections option if item is archived
|
||||
protected readonly showAssignToCollections = computed(() => {
|
||||
if (CipherViewLikeUtils.isArchived(this.cipher())) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.organizations()?.length &&
|
||||
this.canAssignCollections() &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher())
|
||||
);
|
||||
});
|
||||
|
||||
// Do NOT show clone option if:
|
||||
// item is archived AND user is not premium user
|
||||
// item is archived AND enforce org data ownership policy is on
|
||||
protected readonly showClone = computed(() => {
|
||||
if (
|
||||
CipherViewLikeUtils.isArchived(this.cipher()) &&
|
||||
(!this.userCanArchive() || this.enforceOrgDataOwnershipPolicy())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return this.cloneable() && !CipherViewLikeUtils.isDeleted(this.cipher());
|
||||
});
|
||||
|
||||
protected readonly showEventLogs = computed(() => {
|
||||
return this.useEvents() && this.cipher().organizationId;
|
||||
});
|
||||
|
||||
protected readonly isLoginCipher = computed(() => {
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher()) === this.CipherType.Login &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher()) &&
|
||||
!CipherViewLikeUtils.isArchived(this.cipher())
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly permissionText = computed(() => {
|
||||
if (!this.cipher().organizationId || this.cipher().collectionIds.length === 0) {
|
||||
return this.i18nService.t("manageCollection");
|
||||
}
|
||||
|
||||
return this.i18nService.t("noAccess");
|
||||
});
|
||||
|
||||
protected readonly hasVisibleLoginOptions = computed(() => {
|
||||
return (
|
||||
this.isLoginCipher() &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher(), "username") ||
|
||||
(this.cipher().viewPassword &&
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher(), "password")) ||
|
||||
this.showTotpCopyButton() ||
|
||||
this.canLaunch())
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly isCardCipher = computed(() => {
|
||||
return CipherViewLikeUtils.getType(this.cipher()) === this.CipherType.Card && !this.isDeleted();
|
||||
});
|
||||
|
||||
protected readonly hasVisibleCardOptions = computed(() => {
|
||||
return (
|
||||
this.isCardCipher() &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher(), "cardNumber") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher(), "securityCode"))
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly isIdentityCipher = computed(() => {
|
||||
if (CipherViewLikeUtils.isArchived(this.cipher()) && !this.userCanArchive()) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher()) === this.CipherType.Identity && !this.isDeleted()
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly hasVisibleIdentityOptions = computed(() => {
|
||||
return (
|
||||
this.isIdentityCipher() &&
|
||||
(CipherViewLikeUtils.hasCopyableValue(this.cipher(), "address") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher(), "email") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher(), "username") ||
|
||||
CipherViewLikeUtils.hasCopyableValue(this.cipher(), "phone"))
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly isSecureNoteCipher = computed(() => {
|
||||
return (
|
||||
CipherViewLikeUtils.getType(this.cipher()) === this.CipherType.SecureNote &&
|
||||
!(this.isDeleted() && this.canRestoreCipher())
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly hasVisibleSecureNoteOptions = computed(() => {
|
||||
return (
|
||||
this.isSecureNoteCipher() && CipherViewLikeUtils.hasCopyableValue(this.cipher(), "secureNote")
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly showMenuDivider = computed(() => {
|
||||
return (
|
||||
this.hasVisibleLoginOptions() ||
|
||||
this.hasVisibleCardOptions() ||
|
||||
this.hasVisibleIdentityOptions() ||
|
||||
this.hasVisibleSecureNoteOptions()
|
||||
);
|
||||
});
|
||||
|
||||
protected clone() {
|
||||
this.onEvent.emit({ type: "clone", item: this.cipher() });
|
||||
}
|
||||
|
||||
protected events() {
|
||||
this.onEvent.emit({ type: "viewEvents", item: this.cipher() });
|
||||
}
|
||||
|
||||
protected archive() {
|
||||
this.onEvent.emit({ type: "archive", items: [this.cipher()] });
|
||||
}
|
||||
|
||||
protected unarchive() {
|
||||
this.onEvent.emit({ type: "unarchive", items: [this.cipher()] });
|
||||
}
|
||||
|
||||
protected restore() {
|
||||
this.onEvent.emit({ type: "restore", items: [this.cipher()] });
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
this.onEvent.emit({ type: "delete", items: [{ cipher: this.cipher() }] });
|
||||
}
|
||||
|
||||
protected attachments() {
|
||||
this.onEvent.emit({ type: "viewAttachments", item: this.cipher() });
|
||||
}
|
||||
|
||||
protected assignToCollections() {
|
||||
this.onEvent.emit({ type: "assignToCollections", items: [this.cipher()] });
|
||||
}
|
||||
|
||||
protected toggleFavorite() {
|
||||
this.onEvent.emit({
|
||||
type: "toggleFavorite",
|
||||
item: this.cipher(),
|
||||
});
|
||||
}
|
||||
|
||||
protected editCipher() {
|
||||
this.onEvent.emit({ type: "editCipher", item: this.cipher() });
|
||||
}
|
||||
|
||||
@HostListener("contextmenu", ["$event"])
|
||||
protected onRightClick(event: MouseEvent) {
|
||||
if (event.shiftKey && event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.disabled() && this.menuTrigger) {
|
||||
this.menuTrigger.toggleMenuOnRightClick(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<td bitCell [ngClass]="RowHeightClass">
|
||||
<div class="tw-flex tw-gap-3">
|
||||
<div aria-hidden="true">
|
||||
<i
|
||||
[class]="
|
||||
'bwi bwi-fw bwi-lg ' +
|
||||
(collection().type === DefaultCollectionType ? 'bwi-user' : 'bwi-collection-shared')
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
<button
|
||||
bitLink
|
||||
[disabled]="disabled()"
|
||||
type="button"
|
||||
class="tw-flex tw-text-start tw-leading-snug tw-truncate"
|
||||
linkType="secondary"
|
||||
title="{{ 'viewCollectionWithName' | i18n: collection().name }}"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ collectionId: collection().id }"
|
||||
queryParamsHandling="merge"
|
||||
appStopProp
|
||||
>
|
||||
<span class="tw-truncate tw-mr-1">{{ collection().name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@if (showOwner()) {
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-hidden lg:tw-table-cell">
|
||||
<app-org-badge
|
||||
[disabled]="disabled()"
|
||||
[organizationId]="collection().organizationId"
|
||||
[organizationName]="collection().organizationId | orgNameFromId: organizations()"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
}
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right"></td>
|
||||
@@ -0,0 +1,26 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, input } from "@angular/core";
|
||||
|
||||
import {
|
||||
CollectionView,
|
||||
CollectionTypes,
|
||||
} from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "tr[appVaultCollectionRow]",
|
||||
templateUrl: "vault-collection-row.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultCollectionRowComponent {
|
||||
protected RowHeightClass = `tw-h-[75px]`;
|
||||
protected DefaultCollectionType = CollectionTypes.DefaultUserCollection;
|
||||
|
||||
protected readonly disabled = input<boolean>();
|
||||
protected readonly collection = input<CollectionView>();
|
||||
protected readonly showOwner = input<boolean>();
|
||||
protected readonly organizations = input<Organization[]>();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
BadgeModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
ScrollLayoutDirective,
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
|
||||
import { OrganizationNameBadgeComponent } from "../organization-badge/organization-name-badge.component";
|
||||
import { PipesModule } from "../pipes/pipes.module";
|
||||
|
||||
import { VaultCipherRowComponent } from "./vault-cipher-row.component";
|
||||
import { VaultCollectionRowComponent } from "./vault-collection-row.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
ScrollingModule,
|
||||
JslibModule,
|
||||
TableModule,
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
BadgeModule,
|
||||
CopyCipherFieldDirective,
|
||||
ScrollLayoutDirective,
|
||||
PremiumBadgeComponent,
|
||||
OrganizationNameBadgeComponent,
|
||||
PipesModule,
|
||||
],
|
||||
declarations: [VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
exports: [VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
})
|
||||
export class VaultItemsModule {}
|
||||
@@ -0,0 +1,93 @@
|
||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout class="tw-pb-8">
|
||||
<div class="tw-px-8 tw-pt-6">
|
||||
<app-header [title]="'vault' | i18n">
|
||||
<vault-new-cipher-menu
|
||||
[canCreateCipher]="true"
|
||||
[canCreateFolder]="true"
|
||||
[canCreateSshKey]="true"
|
||||
(cipherAdded)="addCipher($event)"
|
||||
(folderAdded)="addFolder()"
|
||||
/>
|
||||
</app-header>
|
||||
</div>
|
||||
<div class="tw-mb-[16px]">
|
||||
<app-search />
|
||||
</div>
|
||||
<bit-table [dataSource]="dataSource" layout="fixed">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<!-- Individual or Organization vault -->
|
||||
<th
|
||||
bitCell
|
||||
bitSortable="name"
|
||||
[fn]="sortByName"
|
||||
[class]="showExtraColumn ? 'tw-w-3/6' : 'tw-w-full'"
|
||||
>
|
||||
{{ "name" | i18n }}
|
||||
</th>
|
||||
@if (showOwner()) {
|
||||
<th
|
||||
bitCell
|
||||
bitSortable="owner"
|
||||
[fn]="sortByOwner"
|
||||
class="tw-hidden tw-w-2/6 lg:tw-table-cell"
|
||||
>
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
}
|
||||
<th bitCell class="tw-hidden tw-w-1/6 lg:tw-table-cell tw-text-right tw-font-medium">
|
||||
{{ "options" | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<ng-container *cdkVirtualFor="let item of rows$; templateCacheSize: 0">
|
||||
@if (item.collection) {
|
||||
<tr
|
||||
bitRow
|
||||
appVaultCollectionRow
|
||||
alignContent="middle"
|
||||
[disabled]="disabled()"
|
||||
[collection]="item.collection"
|
||||
[showOwner]="showOwner()"
|
||||
[organizations]="allOrganizations()"
|
||||
></tr>
|
||||
}
|
||||
<!--
|
||||
addAccessStatus check here so ciphers do not show if user
|
||||
has filtered for collections with addAccess
|
||||
-->
|
||||
@if (
|
||||
item.cipher && (!addAccessToggle() || (addAccessToggle() && addAccessStatus() !== 1))
|
||||
) {
|
||||
<tr
|
||||
bitRow
|
||||
appVaultCipherRow
|
||||
alignContent="middle"
|
||||
[disabled]="disabled()"
|
||||
[cipher]="item.cipher"
|
||||
[showOwner]="showOwner()"
|
||||
[showPremiumFeatures]="showPremiumFeatures()"
|
||||
[useEvents]="useEvents()"
|
||||
[viewingOrgVault]="viewingOrgVault()"
|
||||
[cloneable]="canClone$(item) | async"
|
||||
[organizations]="allOrganizations()"
|
||||
[canEditCipher]="canEditCipher(item.cipher)"
|
||||
[canAssignCollections]="canAssignCollections(item.cipher)"
|
||||
[canManageCollection]="canManageCollection(item.cipher)"
|
||||
[canDeleteCipher]="
|
||||
cipherAuthorizationService.canDeleteCipher$(item.cipher, showAdminActions()) | async
|
||||
"
|
||||
[canRestoreCipher]="
|
||||
cipherAuthorizationService.canRestoreCipher$(item.cipher, showAdminActions()) | async
|
||||
"
|
||||
(onEvent)="event($event)"
|
||||
[userCanArchive]="userCanArchive()"
|
||||
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy()"
|
||||
[archiveEnabled]="archiveFeatureEnabled$ | async"
|
||||
></tr>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
309
apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
Normal file
309
apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import { Component, input, output, effect, inject, computed } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
RestrictedCipherType,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import {
|
||||
SortDirection,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
MenuModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
import { OrganizationId } from "@bitwarden/sdk-internal";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { NewCipherMenuComponent, VaultItem, VaultItemEvent } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopHeaderComponent } from "../../../app/layout/header/desktop-header.component";
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { SearchComponent } from "../../../app/layout/search/search.component";
|
||||
|
||||
import { VaultItemsModule } from "./vault-items/vault-items.module";
|
||||
|
||||
// Fixed manual row height required due to how cdk-virtual-scroll works
|
||||
export const RowHeight = 75;
|
||||
export const RowHeightClass = `tw-h-[75px]`;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-list",
|
||||
templateUrl: "vault-list.component.html",
|
||||
imports: [
|
||||
ScrollingModule,
|
||||
TableModule,
|
||||
I18nPipe,
|
||||
AsyncPipe,
|
||||
MenuModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
VaultItemsModule,
|
||||
SearchComponent,
|
||||
DesktopHeaderComponent,
|
||||
NewCipherMenuComponent,
|
||||
],
|
||||
})
|
||||
export class VaultListComponent<C extends CipherViewLike> {
|
||||
protected RowHeight = RowHeight;
|
||||
|
||||
protected readonly disabled = input<boolean>();
|
||||
protected readonly showOwner = input<boolean>();
|
||||
protected readonly useEvents = input<boolean>();
|
||||
protected readonly showPremiumFeatures = input<boolean>();
|
||||
// Encompasses functionality only available from the organization vault context
|
||||
protected readonly showAdminActions = input<boolean>(false);
|
||||
protected readonly allOrganizations = input<Organization[]>([]);
|
||||
protected readonly allCollections = input<CollectionView[]>([]);
|
||||
protected readonly showPermissionsColumn = input<boolean>(false);
|
||||
protected readonly viewingOrgVault = input<boolean>();
|
||||
protected readonly addAccessStatus = input<number>();
|
||||
protected readonly addAccessToggle = input<boolean>();
|
||||
protected readonly activeCollection = input<CollectionView | undefined>();
|
||||
protected readonly userCanArchive = input<boolean>();
|
||||
protected readonly enforceOrgDataOwnershipPolicy = input<boolean>();
|
||||
|
||||
protected readonly ciphers = input<C[]>([]);
|
||||
|
||||
protected readonly collections = input<CollectionView[]>([]);
|
||||
|
||||
protected onEvent = output<VaultItemEvent<C>>();
|
||||
protected onAddCipher = output<CipherType>();
|
||||
protected onAddFolder = output<void>();
|
||||
|
||||
protected cipherAuthorizationService = inject(CipherAuthorizationService);
|
||||
protected restrictedItemTypesService = inject(RestrictedItemTypesService);
|
||||
protected cipherArchiveService = inject(CipherArchiveService);
|
||||
private searchBarService = inject(SearchBarService);
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
protected dataSource = new TableDataSource<VaultItem<C>>();
|
||||
protected selection = new SelectionModel<VaultItem<C>>(true, [], true);
|
||||
private restrictedTypes: RestrictedCipherType[] = [];
|
||||
|
||||
protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;
|
||||
|
||||
constructor() {
|
||||
// Enable the search bar
|
||||
this.searchBarService.setEnabled(true);
|
||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||
|
||||
this.restrictedItemTypesService.restricted$.pipe(takeUntilDestroyed()).subscribe((types) => {
|
||||
this.restrictedTypes = types;
|
||||
this.refreshItems();
|
||||
});
|
||||
|
||||
// Refresh items when collections or ciphers change
|
||||
effect(() => {
|
||||
this.collections();
|
||||
this.ciphers();
|
||||
this.refreshItems();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly showExtraColumn = computed(() => this.showOwner());
|
||||
|
||||
protected event(event: VaultItemEvent<C>) {
|
||||
this.onEvent.emit(event);
|
||||
}
|
||||
|
||||
protected addCipher(type: CipherType) {
|
||||
this.onAddCipher.emit(type);
|
||||
}
|
||||
|
||||
protected addFolder() {
|
||||
this.onAddFolder.emit();
|
||||
}
|
||||
|
||||
protected canClone$(vaultItem: VaultItem<C>): Observable<boolean> {
|
||||
return this.restrictedItemTypesService.restricted$.pipe(
|
||||
switchMap((restrictedTypes) => {
|
||||
// This will check for restrictions from org policies before allowing cloning.
|
||||
const isItemRestricted = restrictedTypes.some(
|
||||
(rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher),
|
||||
);
|
||||
if (isItemRestricted) {
|
||||
return of(false);
|
||||
}
|
||||
return this.cipherAuthorizationService.canCloneCipher$(
|
||||
vaultItem.cipher,
|
||||
this.showAdminActions(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected canEditCipher(cipher: C) {
|
||||
if (cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const organization = this.allOrganizations().find((o) => o.id === cipher.organizationId);
|
||||
return (organization.canEditAllCiphers && this.viewingOrgVault()) || cipher.edit;
|
||||
}
|
||||
|
||||
protected canAssignCollections(cipher: C) {
|
||||
const organization = this.allOrganizations().find((o) => o.id === cipher.organizationId);
|
||||
const editableCollections = this.allCollections().filter((c) => !c.readOnly);
|
||||
|
||||
return (
|
||||
(organization?.canEditAllCiphers && this.viewingOrgVault()) ||
|
||||
(CipherViewLikeUtils.canAssignToCollections(cipher) && editableCollections.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected canManageCollection(cipher: C) {
|
||||
// If the cipher is not part of an organization (personal item), user can manage it
|
||||
if (cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for admin access in AC vault
|
||||
if (this.showAdminActions()) {
|
||||
const organization = this.allOrganizations().find((o) => o.id === cipher.organizationId);
|
||||
// If the user is an admin, they can delete an unassigned cipher
|
||||
if (cipher.collectionIds.length === 0) {
|
||||
return organization?.canEditUnmanagedCollections === true;
|
||||
}
|
||||
|
||||
if (
|
||||
organization?.permissions.editAnyCollection ||
|
||||
(organization?.allowAdminAccessToAllCollectionItems && organization.isAdmin)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeCollection()) {
|
||||
return this.activeCollection().manage === true;
|
||||
}
|
||||
|
||||
return this.allCollections()
|
||||
.filter((c) => cipher.collectionIds.includes(c.id as any))
|
||||
.some((collection) => collection.manage);
|
||||
}
|
||||
|
||||
private refreshItems() {
|
||||
const collections: VaultItem<C>[] =
|
||||
this.collections()?.map((collection) => ({ collection })) || [];
|
||||
const ciphers: VaultItem<C>[] = this.ciphers()
|
||||
.filter(
|
||||
(cipher) =>
|
||||
!this.restrictedItemTypesService.isCipherRestricted(cipher, this.restrictedTypes),
|
||||
)
|
||||
.map((cipher) => ({ cipher }));
|
||||
const items: VaultItem<C>[] = [].concat(collections).concat(ciphers);
|
||||
|
||||
this.dataSource.data = items;
|
||||
}
|
||||
|
||||
protected assignToCollections() {
|
||||
this.event({
|
||||
type: "assignToCollections",
|
||||
items: this.selection.selected
|
||||
.filter((item) => item.cipher !== undefined)
|
||||
.map((item) => item.cipher),
|
||||
});
|
||||
}
|
||||
|
||||
protected showAssignToCollections(): boolean {
|
||||
// When the user doesn't belong to an organization, hide assign to collections
|
||||
if (this.allOrganizations().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selection.selected.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasPersonalItems = this.hasPersonalItems();
|
||||
const uniqueCipherOrgIds = this.getUniqueOrganizationIds();
|
||||
const hasEditableCollections = this.allCollections().some((collection) => {
|
||||
return !collection.readOnly;
|
||||
});
|
||||
|
||||
// Return false if items are from different organizations
|
||||
if (uniqueCipherOrgIds.size > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If all selected items are personal, return based on personal items
|
||||
if (uniqueCipherOrgIds.size === 0 && hasEditableCollections) {
|
||||
return hasPersonalItems;
|
||||
}
|
||||
|
||||
const [orgId] = uniqueCipherOrgIds;
|
||||
const organization = this.allOrganizations().find((o) => o.id === orgId);
|
||||
|
||||
const canEditOrManageAllCiphers = organization?.canEditAllCiphers && this.viewingOrgVault();
|
||||
|
||||
const collectionNotSelected =
|
||||
this.selection.selected.filter((item) => item.collection).length === 0;
|
||||
|
||||
return (
|
||||
(canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) &&
|
||||
collectionNotSelected &&
|
||||
hasEditableCollections
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
|
||||
*/
|
||||
protected sortByName = (a: VaultItem<C>, b: VaultItem<C>, direction: SortDirection) => {
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
protected sortByOwner = (a: VaultItem<C>, b: VaultItem<C>, direction: SortDirection) => {
|
||||
const getOwnerName = (item: VaultItem<C>): string => {
|
||||
if (item.cipher) {
|
||||
return (item.cipher.organizationId as string) || "";
|
||||
} else if (item.collection) {
|
||||
return (item.collection.organizationId as string) || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const ownerA = getOwnerName(a);
|
||||
const ownerB = getOwnerName(b);
|
||||
|
||||
return ownerA.localeCompare(ownerB);
|
||||
};
|
||||
|
||||
private compareNames(a: VaultItem<C>, b: VaultItem<C>): number {
|
||||
const getName = (item: VaultItem<C>) => item.collection?.name || item.cipher?.name;
|
||||
return getName(a)?.localeCompare(getName(b)) ?? -1;
|
||||
}
|
||||
|
||||
private hasPersonalItems(): boolean {
|
||||
return this.selection.selected.some(({ cipher }) => !cipher?.organizationId);
|
||||
}
|
||||
|
||||
private allCiphersHaveEditAccess(): boolean {
|
||||
return this.selection.selected
|
||||
.filter(({ cipher }) => cipher)
|
||||
.every(({ cipher }) => cipher?.edit && cipher?.viewPassword);
|
||||
}
|
||||
|
||||
private getUniqueOrganizationIds(): Set<string | [] | OrganizationId> {
|
||||
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,93 @@
|
||||
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
|
||||
<app-vault-items-v2
|
||||
<div id="vault" class="vault vault-v2">
|
||||
<app-vault-list
|
||||
id="items"
|
||||
class="items"
|
||||
[activeCipherId]="cipherId"
|
||||
(onCipherClicked)="viewCipher($event)"
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
[allOrganizations]="allOrganizations"
|
||||
[disabled]="refreshing"
|
||||
[showOwner]="true"
|
||||
[showPremiumFeatures]="canAccessPremium"
|
||||
[useEvents]="false"
|
||||
[showAdminActions]="false"
|
||||
[userCanArchive]="userCanArchive$ | async"
|
||||
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy$ | async"
|
||||
(onEvent)="onVaultItemsEvent($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"
|
||||
(onAddFolder)="addFolder()"
|
||||
/>
|
||||
@if (!!action()) {
|
||||
<div class="details">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher()"
|
||||
[action]="action()"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher($event)"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher($event)"
|
||||
(onCancel)="cancelCipher()"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
@if (action() === "view") {
|
||||
<app-cipher-view [cipher]="cipher()" [collections]="collections"> </app-cipher-view>
|
||||
}
|
||||
@if (action() === "add" || action() === "edit" || action() === "clone") {
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<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>
|
||||
<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 class="tw-bg-bg-secondary tw-px-8 tw-pt-6">
|
||||
<app-header [title]="headerTitleKey() | i18n">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
[label]="'close' | i18n"
|
||||
[disabled]="formDisabled"
|
||||
(click)="cancelCipher()"
|
||||
></button>
|
||||
</app-header>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (action() !== "add" && action() !== "edit" && action() !== "view" && action() !== "clone") {
|
||||
<div id="logo" class="logo">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
</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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { VaultItemEvent as BaseVaultItemEvent } from "@bitwarden/vault";
|
||||
import { CollectionPermission } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
|
||||
// Extend base events with web-specific events
|
||||
export type VaultItemEvent<C extends CipherViewLike> =
|
||||
| { type: "viewAttachments"; item: C }
|
||||
| BaseVaultItemEvent<C>
|
||||
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||
| {
|
||||
type: "viewCollectionAccess";
|
||||
@@ -13,15 +13,4 @@ export type VaultItemEvent<C extends CipherViewLike> =
|
||||
readonly: boolean;
|
||||
initialPermission?: CollectionPermission;
|
||||
}
|
||||
| { type: "viewEvents"; item: C }
|
||||
| { type: "editCollection"; item: CollectionView; readonly: boolean }
|
||||
| { type: "clone"; item: C }
|
||||
| { type: "restore"; items: C[] }
|
||||
| { type: "delete"; items: VaultItem<C>[] }
|
||||
| { type: "copyField"; item: C; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: C[] }
|
||||
| { type: "assignToCollections"; items: C[] }
|
||||
| { type: "archive"; items: C[] }
|
||||
| { type: "unarchive"; items: C[] }
|
||||
| { type: "toggleFavorite"; item: C }
|
||||
| { type: "editCipher"; item: C };
|
||||
| { type: "editCollection"; item: CollectionView; readonly: boolean };
|
||||
|
||||
@@ -11,9 +11,8 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuModule, TableModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { RoutedVaultFilterService, RoutedVaultFilterModel } from "@bitwarden/vault";
|
||||
import { RoutedVaultFilterService, RoutedVaultFilterModel, VaultItem } from "@bitwarden/vault";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
|
||||
describe("VaultItemsComponent", () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||
import { OrganizationId } from "@bitwarden/sdk-internal";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/vault";
|
||||
import { RoutedVaultFilterService, VaultItem } from "@bitwarden/vault";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
CollectionPermission,
|
||||
convertToPermission,
|
||||
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemEvent } from "./vault-item-event";
|
||||
|
||||
// Fixed manual row height required due to how cdk-virtual-scroll works
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { GetOrgNameFromIdPipe } from "@bitwarden/vault";
|
||||
|
||||
import { GetGroupNameFromIdPipe } from "./get-group-name.pipe";
|
||||
import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe";
|
||||
|
||||
@NgModule({
|
||||
declarations: [GetOrgNameFromIdPipe, GetGroupNameFromIdPipe],
|
||||
|
||||
@@ -112,6 +112,7 @@ import {
|
||||
OrganizationFilter,
|
||||
VaultItemsTransferService,
|
||||
DefaultVaultItemsTransferService,
|
||||
VaultItem,
|
||||
} from "@bitwarden/vault";
|
||||
import { UnifiedUpgradePromptService } from "@bitwarden/web-vault/app/billing/individual/upgrade/services";
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
@@ -134,7 +135,6 @@ import {
|
||||
VaultItemDialogMode,
|
||||
VaultItemDialogResult,
|
||||
} from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||
import { VaultItem } from "../components/vault-items/vault-item";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { VaultItemsComponent } from "../components/vault-items/vault-items.component";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
|
||||
16
libs/vault/src/components/vault-item-event.ts
Normal file
16
libs/vault/src/components/vault-item-event.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { VaultItem } from "@bitwarden/vault";
|
||||
|
||||
export type VaultItemEvent<C extends CipherViewLike> =
|
||||
| { type: "viewAttachments"; item: C }
|
||||
| { type: "viewEvents"; item: C }
|
||||
| { type: "clone"; item: C }
|
||||
| { type: "restore"; items: C[] }
|
||||
| { type: "delete"; items: VaultItem<C>[] }
|
||||
| { type: "copyField"; item: C; field: "username" | "password" | "totp" }
|
||||
| { type: "moveToFolder"; items: C[] }
|
||||
| { type: "assignToCollections"; items: C[] }
|
||||
| { type: "archive"; items: C[] }
|
||||
| { type: "unarchive"; items: C[] }
|
||||
| { type: "toggleFavorite"; item: C }
|
||||
| { type: "editCipher"; item: C };
|
||||
@@ -12,6 +12,7 @@ export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directi
|
||||
export { OrgIconDirective } from "./components/org-icon.directive";
|
||||
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
|
||||
export { DarkImageSourceDirective } from "./components/dark-image-source.directive";
|
||||
export { GetOrgNameFromIdPipe } from "./pipes/get-organization-name.pipe";
|
||||
|
||||
export * from "./cipher-view";
|
||||
export * from "./cipher-form";
|
||||
@@ -30,6 +31,8 @@ export * from "./components/carousel";
|
||||
export * from "./components/new-cipher-menu/new-cipher-menu.component";
|
||||
export * from "./components/permit-cipher-details-popover/permit-cipher-details-popover.component";
|
||||
export * from "./components/vault-items-transfer";
|
||||
export { VaultItem } from "./components/vault-item";
|
||||
export { VaultItemEvent } from "./components/vault-item-event";
|
||||
|
||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||
|
||||
Reference in New Issue
Block a user