1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 02:23:25 +00:00

[PM-18768] Migrate vault cipher list (#18522)

* migrated vault cipher list

* added back `rounded` prop to `bit-layout`

* moved account switcher to right corner

* moved username below cipher item name

* fixed spacing to align with send pages

* removed commented out

* fixed options buttons overflowing if has launch

* fixed "options" label disappearing when width is insufficient

* reverted search component, added search directly to vault-list

* placed new vault cipher list work behind 'desktop-ui-migration-milestone-3' feature flag

* reverted scss changes

* added back search bar when FF not enabled

* fixed owner column responsiveness (set to table width instead of screen)

* fixed 'owner' column responsiveness

* hide 'owner' column at 'md' breakpoint

* Remove duplicate badge component and org name pipe

* Convert to standalone

* Added back translations

* used correct 'tw' variants for 'px'

* extended existing `item-footer` component

* removed unused `showGroups()` from `vault-cipher-row`

* removed 'addAccess' from `vault-list.component`

* removed more unused, separated 'cipher collections' from 'filter collections'

* converted `vault-wrapper` to use signal

* updated original 'vault.component' to reflect main

* fixed `templateUrl`, merge fix

* changed to `getFeatureFlag$`

* fixes for `item-footer` and `vault-collection-row`

* fixed lint error

* - replaced using global css with tailwind
- added functionality and ui for empty states
- moved search and vault header from 'vault-list' to 'vault component'

* fixed accessing `this.userCanArchive$`

* converted more to tailwind in vault component

* removed unused 'selection' from `vault-list`

* Fix flashing vault list

* Move app-header title to routing module

* Remove broken half-migrated new form

* removed unnecessary `this.organizations$` block

* removed `firstSetup$`, cleaned up unused, separated 'delete' and 'restore' handling for footer and cipher menu

* used desktop 'launch' functionality

* moved 'bit-no-items' into `vault-list`

* removed unused locales

* aligned `handleDelete` and `handleRestore` with original desktop functionality

* Fix linting and tests

* Move no-items out of table similar to send.

* Re-add newline end of messges.json

* Remove events

* Hide copy buttons if there is nothing to copy. Simplify

* fix

* Get rid of unused copyField

* Use dropdown button in vault list instead

* Fix linting

* removed unused imports

* updated `vault-orig` to current in main

* fixed `vault-orig` templateUrl

* fixed import order, removed unused `combineLatest` block

* fixed `onVaultItemsEvent` "delete"

* removed duplicate rendering of logo

* preserve cipher view after 'cancel'

* filter from `allCiphers`

* moved `enforceOrganizationDataOwnership` call inside "syncCompleted" block

* converted `showAddCipherBtn` to observable

* removed unused

* added `submitButtonText` to `app-vault-item-footer`

* removed filtering restricted item types

* fixed `cancelCipher` pass in and set `cipherId`

* updated `submitButtonText`

* updated `vault-orig` to current `vault` in main

---------

Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
Leslie Xiong
2026-02-26 16:41:02 -05:00
committed by GitHub
parent 4c2aec162d
commit b3439482fa
29 changed files with 2804 additions and 565 deletions

View File

@@ -54,7 +54,7 @@ import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create
import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component";
import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault-v3/vault.component";
import { VaultWrapperComponent } from "../vault/app/vault-v3/vault-wrapper.component";
import { DesktopLayoutComponent } from "./layout/desktop-layout.component";
import { SendComponent } from "./tools/send/send.component";
@@ -358,7 +358,8 @@ const routes: Routes = [
children: [
{
path: "new-vault",
component: VaultComponent,
component: VaultWrapperComponent,
data: { pageTitle: { key: "vault" } } satisfies RouteDataProperties,
},
{
path: "new-sends",

View File

@@ -11,6 +11,9 @@
"favorites": {
"message": "Favorites"
},
"unfavorite": {
"message": "Unfavorite"
},
"types": {
"message": "Types"
},
@@ -586,6 +589,12 @@
"editedItem": {
"message": "Item saved"
},
"itemAddedToFavorites": {
"message": "Item added to favorites"
},
"itemRemovedFromFavorites": {
"message": "Item removed from favorites"
},
"deleteItem": {
"message": "Delete item"
},
@@ -1555,6 +1564,18 @@
"unknown": {
"message": "Unknown"
},
"copyAddress": {
"message": "Copy address"
},
"copyPhone": {
"message": "Copy phone"
},
"copyNote": {
"message": "Copy note"
},
"copyVerificationCode": {
"message": "Copy verification code"
},
"copyUsername": {
"message": "Copy username"
},
@@ -2082,6 +2103,24 @@
"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"
},
@@ -2378,6 +2417,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"
},
@@ -4086,6 +4128,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": {
@@ -4590,6 +4635,36 @@
"whyAmISeeingThis": {
"message": "Why am I seeing this?"
},
"organizationIsSuspended": {
"message": "Organization is suspended"
},
"organizationIsSuspendedDesc": {
"message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance."
},
"noItemsInTrash": {
"message": "No items in trash"
},
"noItemsInTrashDesc": {
"message": "Items you delete will appear here and be permanently deleted after 30 days"
},
"noItemsInVault": {
"message": "No items in the vault"
},
"emptyVaultDescription": {
"message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here."
},
"emptyFavorites": {
"message": "You haven't favorited any items"
},
"emptyFavoritesDesc": {
"message": "Add frequently used items to favorites for quick access."
},
"noSearchResults": {
"message": "No search results returned"
},
"clearFiltersOrTryAnother": {
"message": "Clear filters or try another search term"
},
"sendPasswordHelperText": {
"message": "Individuals will need to enter the password to view this Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."

View File

@@ -55,13 +55,6 @@ export class OrganizationFilterComponent {
protected applyFilter(event: Event, organization: TreeNode<OrganizationFilter>) {
event.stopPropagation();
if (!organization.node.enabled) {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("disabledOrganizationFilterError"),
});
return;
}
this.vaultFilterService.setOrganizationFilter(organization.node);
const filter = this.activeFilter();

View File

@@ -0,0 +1,195 @@
<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-flex tw-flex-col tw-w-full">
<button
bitLink
class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug"
[disabled]="disabled()"
(click)="viewCipher()"
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>
}
}
<span class="tw-text-sm tw-text-muted" appStopProp>{{ subtitle() }}</span>
</div>
</td>
@if (showOwner()) {
<td bitCell [ngClass]="RowHeightClass" class="tw-hidden @md:tw-table-cell">
<app-org-badge
[disabled]="disabled()"
[organizationId]="cipher().organizationId"
[organizationName]="cipher().organizationId | orgNameFromId: organizations()"
appStopProp
>
</app-org-badge>
</td>
}
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right tw-whitespace-nowrap">
@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()) {
<button
[disabled]="disabled()"
bitIconButton="bwi-external-link"
type="button"
appStopProp
label="{{ 'launch' | i18n }}"
(click)="handleLaunch()"
></button>
}
@if (showCopyButton()) {
<button
[bitMenuTriggerFor]="copyOptions"
[disabled]="disabled()"
bitIconButton="bwi-clone"
type="button"
appStopProp
label="{{ 'copyValue' | i18n }}"
></button>
<bit-menu #copyOptions>
@for (copyField of copyFields(); track copyField.field) {
<button type="button" bitMenuItem [appCopyField]="copyField.field" [cipher]="cipher()">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ copyField.title | 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()) {
<button type="button" bitMenuItem (click)="handleLaunch()">
<i class="bwi bwi-fw bwi-external-link" aria-hidden="true"></i>
{{ "launch" | i18n }}
</button>
}
@for (copyField of copyFields(); track copyField.field) {
<button type="button" bitMenuItem [appCopyField]="copyField.field" [cipher]="cipher()">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ copyField.title | i18n }}
</button>
}
@if (showMenuDivider()) {
<bit-menu-divider />
}
@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 (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>

View File

@@ -0,0 +1,301 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import { Component, HostListener, ViewChild, computed, inject, input, output } from "@angular/core";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge/premium-badge.component";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import {
CipherViewLike,
CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import {
AriaDisableDirective,
BitIconButtonComponent,
MenuModule,
MenuTriggerForDirective,
TooltipDirective,
TableModule,
} from "@bitwarden/components";
import {
CopyAction,
CopyCipherFieldDirective,
GetOrgNameFromIdPipe,
OrganizationNameBadgeComponent,
} from "@bitwarden/vault";
import { VaultItemEvent } from "./vault-item-event";
/** Configuration for a copyable field */
interface CopyFieldConfig {
field: CopyAction;
title: string;
}
// 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",
imports: [
NgClass,
JslibModule,
TableModule,
AriaDisableDirective,
OrganizationNameBadgeComponent,
TooltipDirective,
BitIconButtonComponent,
MenuModule,
CopyCipherFieldDirective,
PremiumBadgeComponent,
GetOrgNameFromIdPipe,
],
})
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 showPremiumFeatures = input<boolean>();
protected readonly useEvents = input<boolean>();
protected readonly cloneable = input<boolean>();
protected readonly organizations = input<Organization[]>();
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 platformUtilsService = inject(PlatformUtilsService);
protected readonly showArchiveButton = computed(() => {
return (
this.archiveEnabled() &&
!this.cipher().organizationId &&
!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(() => {
return (
CipherViewLikeUtils.isArchived(this.cipher()) && !CipherViewLikeUtils.isDeleted(this.cipher())
);
});
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 handleLaunch() {
const launchUri = CipherViewLikeUtils.getLaunchUri(this.cipher());
this.platformUtilsService.launchUri(launchUri);
}
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 showMenuDivider = computed(() => this.showCopyButton() || this.canLaunch());
/**
* Returns the list of copyable fields based on cipher type.
* Used to render copy menu items dynamically.
*/
protected readonly copyFields = computed((): CopyFieldConfig[] => {
const cipher = this.cipher();
// No copy options for deleted or archived items
if (this.isDeleted() || CipherViewLikeUtils.isArchived(cipher)) {
return [];
}
const cipherType = CipherViewLikeUtils.getType(cipher);
switch (cipherType) {
case CipherType.Login: {
const fields: CopyFieldConfig[] = [{ field: "username", title: "copyUsername" }];
if (cipher.viewPassword) {
fields.push({ field: "password", title: "copyPassword" });
}
if (
CipherViewLikeUtils.getLogin(cipher).totp &&
(cipher.organizationUseTotp || this.showPremiumFeatures())
) {
fields.push({ field: "totp", title: "copyVerificationCode" });
}
return fields;
}
case CipherType.Card:
return [
{ field: "cardNumber", title: "copyNumber" },
{ field: "securityCode", title: "copySecurityCode" },
];
case CipherType.Identity:
return [
{ field: "username", title: "copyUsername" },
{ field: "email", title: "copyEmail" },
{ field: "phone", title: "copyPhone" },
{ field: "address", title: "copyAddress" },
];
case CipherType.SecureNote:
return [{ field: "secureNote", title: "copyNote" }];
default:
return [];
}
});
/**
* Determines if the copy button should be shown.
* Returns true only if at least one field has a copyable value.
*/
protected readonly showCopyButton = computed(() => {
const cipher = this.cipher();
return this.copyFields().some(({ field }) =>
CipherViewLikeUtils.hasCopyableValue(cipher, field),
);
});
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() });
}
protected viewCipher() {
this.onEvent.emit({ type: "viewCipher", 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);
}
}
}

View File

@@ -0,0 +1,40 @@
<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 @md: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>

View File

@@ -0,0 +1,38 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import { Component, input } from "@angular/core";
import { RouterLink } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
CollectionView,
CollectionTypes,
} from "@bitwarden/common/admin-console/models/collections";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { TableModule } from "@bitwarden/components";
import { GetOrgNameFromIdPipe, OrganizationNameBadgeComponent } 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[appVaultCollectionRow]",
templateUrl: "vault-collection-row.component.html",
imports: [
TableModule,
NgClass,
JslibModule,
RouterLink,
OrganizationNameBadgeComponent,
GetOrgNameFromIdPipe,
],
})
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[]>();
}

View File

@@ -0,0 +1,7 @@
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { VaultItemEvent as BaseVaultItemEvent } from "@bitwarden/vault";
// Extend base events with desktop-specific events
export type VaultItemEvent<C extends CipherViewLike> =
| BaseVaultItemEvent<C>
| { type: "viewCipher"; item: C };

View File

@@ -0,0 +1,103 @@
@if (showPremiumCallout()) {
<div class="tw-m-4">
<bit-callout type="default" [title]="'premiumSubscriptionEnded' | i18n">
<ng-container>
<div>
{{ "premiumSubscriptionEndedDesc" | i18n }}
</div>
<a bitLink href="#" appStopClick (click)="navigateToGetPremium()">
{{ "restartPremium" | i18n }}
</a>
</ng-container>
</bit-callout>
</div>
}
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout>
<div class="tw-mb-4 tw-px-6 tw-@container">
<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-1/6 @md:tw-table-cell"
>
{{ "owner" | i18n }}
</th>
}
<th bitCell class="tw-w-2/6 tw-text-right tw-font-medium">
{{ "options" | i18n }}
</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<ng-container *cdkVirtualFor="let item of rows$; trackBy: trackByFn; templateCacheSize: 0">
@if (item.collection) {
<tr
bitRow
appVaultCollectionRow
alignContent="middle"
[disabled]="disabled()"
[collection]="item.collection"
[showOwner]="showOwner()"
[organizations]="allOrganizations()"
></tr>
}
@if (item.cipher) {
<tr
bitRow
appVaultCipherRow
alignContent="middle"
[disabled]="disabled()"
[cipher]="item.cipher"
[showOwner]="showOwner()"
[showPremiumFeatures]="showPremiumFeatures()"
[cloneable]="canClone$(item) | async"
[organizations]="allOrganizations()"
[canEditCipher]="canEditCipher(item.cipher)"
[canAssignCollections]="canAssignCollections(item.cipher)"
[canManageCollection]="canManageCollection(item.cipher)"
[canDeleteCipher]="cipherAuthorizationService.canDeleteCipher$(item.cipher) | async"
[canRestoreCipher]="cipherAuthorizationService.canRestoreCipher$(item.cipher) | async"
(onEvent)="event($event)"
[userCanArchive]="userCanArchive()"
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy()"
[archiveEnabled]="archiveFeatureEnabled$ | async"
></tr>
}
</ng-container>
</ng-template>
</bit-table>
</div>
@if (isEmpty()) {
<bit-no-items [icon]="emptyStateItem()?.icon">
<div slot="title">
{{ emptyStateItem()?.title | i18n }}
</div>
<p slot="description" bitTypography="body2" class="tw-max-w-md tw-text-center">
{{ emptyStateItem()?.description | i18n }}
</p>
@if (showAddCipherBtn()) {
<vault-new-cipher-menu
[canCreateCipher]="true"
[canCreateFolder]="true"
[canCreateSshKey]="true"
(cipherAdded)="addCipher($event)"
(folderAdded)="addFolder()"
slot="button"
/>
}
</bit-no-items>
}
</cdk-virtual-scroll-viewport>

View File

@@ -0,0 +1,212 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { BitSvg } from "@bitwarden/assets/svg";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.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,
NoItemsModule,
CalloutComponent,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { NewCipherMenuComponent, VaultItem } from "@bitwarden/vault";
import { VaultCipherRowComponent } from "./vault-items/vault-cipher-row.component";
import { VaultCollectionRowComponent } from "./vault-items/vault-collection-row.component";
import { VaultItemEvent } from "./vault-items/vault-item-event";
// Fixed manual row height required due to how cdk-virtual-scroll works
export const RowHeight = 75;
export const RowHeightClass = `tw-h-[75px]`;
type EmptyStateItem = {
title: string;
description: string;
icon: BitSvg;
};
// 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,
VaultCollectionRowComponent,
VaultCipherRowComponent,
NoItemsModule,
NewCipherMenuComponent,
CalloutComponent,
],
})
export class VaultListComponent<C extends CipherViewLike> {
protected RowHeight = RowHeight;
protected readonly disabled = input<boolean>();
protected readonly showOwner = input<boolean>();
protected readonly showPremiumFeatures = input<boolean>();
protected readonly allOrganizations = input<Organization[]>([]);
protected readonly allCollections = input<CollectionView[]>([]);
protected readonly userCanArchive = input<boolean>();
protected readonly enforceOrgDataOwnershipPolicy = input<boolean>();
protected readonly placeholderText = input<string>("");
protected readonly ciphers = input<C[]>([]);
protected readonly collections = input<CollectionView[]>([]);
protected readonly isEmpty = input<boolean>();
protected readonly showAddCipherBtn = input<boolean>();
protected readonly emptyStateItem = input<EmptyStateItem>();
readonly showPremiumCallout = input<boolean>(false);
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 premiumUpgradePromptService = inject(PremiumUpgradePromptService);
protected dataSource = new TableDataSource<VaultItem<C>>();
private restrictedTypes: RestrictedCipherType[] = [];
protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;
constructor() {
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);
}),
);
}
protected canEditCipher(cipher: C) {
if (cipher.organizationId == null) {
return true;
}
return cipher.edit;
}
protected canAssignCollections(cipher: C) {
const editableCollections = this.allCollections().filter((c) => !c.readOnly);
return 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;
}
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().map((cipher) => ({ cipher }));
const items: VaultItem<C>[] = [].concat(collections).concat(ciphers);
this.dataSource.data = items;
}
/**
* 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;
}
protected trackByFn(index: number, item: VaultItem<C>) {
return item.cipher?.id || item.collection?.id || index;
}
async navigateToGetPremium() {
await this.premiumUpgradePromptService.promptForPremium();
}
}

View File

@@ -0,0 +1,86 @@
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
<app-vault-items-v2
id="items"
class="items"
[activeCipherId]="cipherId"
(onCipherClicked)="viewCipher($event)"
(onCipherRightClicked)="viewCipherMenu($event)"
(onAddCipher)="addCipher($event)"
(onAddFolder)="addFolder()"
[showPremiumCallout]="showPremiumCallout$ | async"
>
</app-vault-items-v2>
@if (!!action) {
<div class="details">
<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"
[submitButtonText]="submitButtonText()"
></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)"
>
<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>
}
@if (!["add", "edit", "view", "clone"].includes(action)) {
<div id="logo" class="logo">
<div class="content">
<div class="inner-content">
@if (activeFilter.isArchived && !(hasArchivedCiphers$ | async)) {
<bit-no-items [icon]="itemTypesIcon">
<div slot="title">
{{ "noItemsInArchive" | i18n }}
</div>
<p slot="description" bitTypography="body2" class="tw-max-w-md tw-text-center">
{{ "noItemsInArchiveDesc" | i18n }}
</p>
</bit-no-items>
} @else {
<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

View File

@@ -0,0 +1,28 @@
import { CommonModule } from "@angular/common";
import { Component, computed, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { VaultComponent as VaultOrigComponent } from "./vault-orig.component";
import { VaultComponent } from "./vault.component";
// 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-wrapper",
template: '<ng-container *ngComponentOutlet="componentToRender()"></ng-container>',
imports: [CommonModule],
})
export class VaultWrapperComponent {
private configService: ConfigService = inject(ConfigService);
protected readonly useMilestone3 = toSignal(
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone3),
);
protected readonly componentToRender = computed(() =>
this.useMilestone3() ? VaultComponent : VaultOrigComponent,
);
}

View File

@@ -1,38 +1,53 @@
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
<app-vault-items-v2
id="items"
class="items"
[activeCipherId]="cipherId"
(onCipherClicked)="viewCipher($event)"
(onCipherRightClicked)="viewCipherMenu($event)"
(onAddCipher)="addCipher($event)"
(onAddFolder)="addFolder()"
[showPremiumCallout]="showPremiumCallout$ | async"
>
</app-vault-items-v2>
@if (!!action) {
<div class="vault tw-flex tw-h-full">
<div class="tw-flex tw-flex-col tw-w-6/12">
<div class="tw-pt-6 tw-px-6">
<app-header>
<vault-new-cipher-menu
[canCreateCipher]="true"
[canCreateFolder]="true"
[canCreateSshKey]="true"
(cipherAdded)="addCipher($event)"
(folderAdded)="addFolder()"
/>
</app-header>
</div>
<bit-search
class="tw-mb-4 tw-px-6"
[ngModel]="currentSearchText$ | async"
(ngModelChange)="filterSearchText($event)"
[placeholder]="searchPlaceholderText"
appAutofocus
/>
<app-vault-list
class="tw-h-full"
[ciphers]="ciphers"
[collections]="collectionsToDisplay"
[allCollections]="allCollections"
[allOrganizations]="allOrganizations"
[disabled]="refreshing"
[showOwner]="true"
[showPremiumFeatures]="userHasPremium()"
[userCanArchive]="userCanArchive$ | async"
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy$ | async"
[placeholderText]="searchPlaceholderText"
[isEmpty]="isEmpty && !performingInitialLoad"
[showAddCipherBtn]="showAddCipherBtn$ | async"
[emptyStateItem]="emptyState$ | async"
[showPremiumCallout]="showPremiumCallout$ | async"
(onEvent)="onVaultItemsEvent($event)"
(onAddCipher)="addCipher($event)"
(onAddFolder)="addFolder()"
/>
</div>
@if (!!action()) {
<div class="details">
<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"
[submitButtonText]="submitButtonText()"
></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() === "view") {
<app-cipher-view [cipher]="cipher()" [collections]="collections" />
}
@if (action === "add" || action === "edit" || action === "clone") {
@if (action() === "add" || action() === "edit" || action() === "clone") {
<vault-cipher-form
#vaultForm
formId="cipherForm"
@@ -50,7 +65,7 @@
>
<div class="tw-flex tw-items-center tw-gap-2">
{{ "attachments" | i18n }}
<app-premium-badge></app-premium-badge>
<app-premium-badge />
</div>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button>
@@ -60,9 +75,23 @@
</div>
</div>
</div>
<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"
[submitButtonText]="submitButtonText()"
/>
</div>
}
@if (!["add", "edit", "view", "clone"].includes(action)) {
@if (!["add", "edit", "view", "clone"].includes(action())) {
<div id="logo" class="logo">
<div class="content">
<div class="inner-content">
@@ -83,4 +112,3 @@
</div>
}
</div>
<ng-template #folderAddEdit></ng-template>

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,11 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { IconButtonModule, MenuModule } from "@bitwarden/components";
import { CopyCipherFieldDirective, CopyCipherFieldService } from "@bitwarden/vault";
import { OrganizationNameBadgeComponent } from "../../individual-vault/organization-badge/organization-name-badge.component";
import {
CopyCipherFieldDirective,
CopyCipherFieldService,
OrganizationNameBadgeComponent,
} from "@bitwarden/vault";
import { VaultCipherRowComponent } from "./vault-cipher-row.component";
@@ -45,7 +47,7 @@ describe("VaultCipherRowComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VaultCipherRowComponent, OrganizationNameBadgeComponent],
declarations: [VaultCipherRowComponent],
imports: [
CommonModule,
RouterModule.forRoot([]),
@@ -53,6 +55,7 @@ describe("VaultCipherRowComponent", () => {
IconButtonModule,
JslibModule,
CopyCipherFieldDirective,
OrganizationNameBadgeComponent,
],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },

View File

@@ -1,11 +1,12 @@
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: "copyField"; item: C; field: "username" | "password" | "totp" }
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
| {
type: "viewCollectionAccess";
@@ -13,15 +14,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 };

View File

@@ -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", () => {

View File

@@ -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

View File

@@ -1,12 +1,14 @@
import { NgModule } from "@angular/core";
import { OrganizationNameBadgeComponent } from "@bitwarden/vault";
import { SharedModule } from "../../../shared/shared.module";
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
/**
* @deprecated Use `OrganizationNameBadgeComponent` directly since it is now standalone.
*/
@NgModule({
imports: [SharedModule],
declarations: [OrganizationNameBadgeComponent],
imports: [SharedModule, OrganizationNameBadgeComponent],
exports: [OrganizationNameBadgeComponent],
})
export class OrganizationBadgeModule {}

View File

@@ -1,10 +1,12 @@
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],
imports: [GetOrgNameFromIdPipe],
declarations: [GetGroupNameFromIdPipe],
exports: [GetOrgNameFromIdPipe, GetGroupNameFromIdPipe],
})
export class PipesModule {}

View File

@@ -99,6 +99,7 @@ import {
OrganizationFilter,
VaultItemsTransferService,
DefaultVaultItemsTransferService,
VaultItem,
} from "@bitwarden/vault";
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
@@ -115,7 +116,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";

View File

@@ -87,6 +87,7 @@ export enum FeatureFlag {
/* Desktop */
DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1",
DesktopUiMigrationMilestone2 = "desktop-ui-migration-milestone-2",
DesktopUiMigrationMilestone3 = "desktop-ui-migration-milestone-3",
/* UIF */
RouterFocusManagement = "router-focus-management",
@@ -183,6 +184,7 @@ export const DefaultFeatureFlagValue = {
/* Desktop */
[FeatureFlag.DesktopUiMigrationMilestone1]: FALSE,
[FeatureFlag.DesktopUiMigrationMilestone2]: FALSE,
[FeatureFlag.DesktopUiMigrationMilestone3]: FALSE,
/* UIF */
[FeatureFlag.RouterFocusManagement]: FALSE,

View File

@@ -1,13 +1,16 @@
// 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
@@ -15,7 +18,7 @@ import { OrganizationId } from "@bitwarden/sdk-internal";
@Component({
selector: "app-org-badge",
templateUrl: "organization-name-badge.component.html",
standalone: false,
imports: [RouterModule, JslibModule, BadgeModule],
})
export class OrganizationNameBadgeComponent implements OnChanges {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals

View File

@@ -0,0 +1,15 @@
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: "moveToFolder"; items: C[] }
| { type: "assignToCollections"; items: C[] }
| { type: "archive"; items: C[] }
| { type: "unarchive"; items: C[] }
| { type: "toggleFavorite"; item: C }
| { type: "editCipher"; item: C };

View File

@@ -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,9 @@ 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 * from "./components/organization-name-badge/organization-name-badge.component";
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
export { SshImportPromptService } from "./services/ssh-import-prompt.service";

View File

@@ -6,7 +6,6 @@ import { OrganizationId } from "@bitwarden/sdk-internal";
@Pipe({
name: "orgNameFromId",
pure: true,
standalone: false,
})
export class GetOrgNameFromIdPipe implements PipeTransform {
transform(value: string | OrganizationId, organizations: Organization[]) {