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:
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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[]>();
|
||||
}
|
||||
@@ -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 };
|
||||
103
apps/desktop/src/vault/app/vault-v3/vault-list.component.html
Normal file
103
apps/desktop/src/vault/app/vault-v3/vault-list.component.html
Normal 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>
|
||||
212
apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
Normal file
212
apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
1043
apps/desktop/src/vault/app/vault-v3/vault-orig.component.ts
Normal file
1043
apps/desktop/src/vault/app/vault-v3/vault-orig.component.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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 } },
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
15
libs/vault/src/components/vault-item-event.ts
Normal file
15
libs/vault/src/components/vault-item-event.ts
Normal 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 };
|
||||
@@ -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";
|
||||
|
||||
@@ -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[]) {
|
||||
Reference in New Issue
Block a user