Skip to content

Commit

Permalink
[PM-17186] - Add Card and Identity sub-headers to Autofill Suggestions (
Browse files Browse the repository at this point in the history
#13068)

* autofill section headers

* dry up code. fix groupByType state

* revert change to div

* add collapsible code back in

* add compact mode styling and DRYd up template

* fix font weight

* simplify grouping logic

* rearrange code back to original ordering

* use input method in favor of get/set

* fix count

* set initial value for ciphers and groupByType
  • Loading branch information
jaasen-livefront authored Feb 4, 2025
1 parent 2c36744 commit b55468e
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
showAutofillButton
[primaryActionAutofill]="clickItemsToAutofillVaultView"
[groupByType]="groupByType()"
></app-vault-list-items-container>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
Expand Down Expand Up @@ -48,6 +49,10 @@ export class AutofillVaultListItemsComponent implements OnInit {

clickItemsToAutofillVaultView = false;

protected groupByType = toSignal(
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)),
);

/**
* Observable that determines whether the empty autofill tip should be shown.
* The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
<bit-section
*ngIf="cipherGroups$().length > 0 || description"
[disableMargin]="disableSectionMargin"
>
<ng-container *ngIf="collapsibleKey">
<button
class="tw-group/vault-section-header hover:tw-bg-primary-100 tw-rounded-md tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
Expand All @@ -18,6 +21,7 @@
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
</bit-disclosure>
</ng-container>

<ng-container *ngIf="!collapsibleKey">
<div class="tw-pl-1">
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
Expand Down Expand Up @@ -48,7 +52,7 @@ <h2 bitTypography="h6">
'tw-hidden': collapsibleKey && !sectionOpenState(),
}"
>
{{ ciphers.length }}
{{ ciphers().length }}
</span>
<span class="tw-pr-1" *ngIf="collapsibleKey">
<i
Expand All @@ -73,69 +77,78 @@ <h2 bitTypography="h6">

<ng-template #itemGroup>
<bit-item-group>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<bit-item *cdkVirtualFor="let cipher of ciphers">
<button
bit-item-content
type="button"
(click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
class="{{ itemHeightClass }}"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
<i
*ngIf="cipher.organizationId"
slot="default-trailing"
appOrgIcon
[tierType]="cipher.organization.productTierType"
[size]="'small'"
[appA11yTitle]="orgIconTooltip(cipher)"
></i>
<i
*ngIf="cipher.hasAttachments"
class="bwi bwi-paperclip bwi-sm"
[appA11yTitle]="'attachments' | i18n"
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<ng-container slot="end">
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button
type="button"
bitBadge
variant="primary"
(click)="doAutofill(cipher)"
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
>
{{ "fill" | i18n }}
</button>
</bit-item-action>
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
<button
type="button"
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</ng-container>
</bit-item>
</cdk-virtual-scroll-viewport>
<ng-container *ngFor="let group of cipherGroups$()">
<ng-container *ngIf="group.subHeaderKey">
<h3 class="tw-text-muted tw-text-xs tw-font-semibold tw-pl-1 tw-mb-1 bit-compact:tw-m-0">
{{ group.subHeaderKey | i18n }}
</h3>
</ng-container>

<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
<button
bit-item-content
type="button"
(click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
class="{{ itemHeightClass }}"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
<i
*ngIf="cipher.organizationId"
slot="default-trailing"
appOrgIcon
[tierType]="cipher.organization.productTierType"
[size]="'small'"
[appA11yTitle]="orgIconTooltip(cipher)"
></i>
<i
*ngIf="cipher.hasAttachments"
class="bwi bwi-paperclip bwi-sm"
[appA11yTitle]="'attachments' | i18n"
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>

<ng-container slot="end">
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button
type="button"
bitBadge
variant="primary"
(click)="doAutofill(cipher)"
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
>
{{ "fill" | i18n }}
</button>
</bit-item-action>
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
<button
type="button"
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</ng-container>
</bit-item>
</cdk-virtual-scroll-viewport>
</ng-container>
</bit-item-group>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
EventEmitter,
inject,
Input,
OnInit,
Output,
Signal,
signal,
ViewChild,
computed,
OnInit,
ChangeDetectionStrategy,
input,
} from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Observable, map } from "rxjs";
Expand All @@ -23,6 +26,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
Expand Down Expand Up @@ -73,6 +77,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
selector: "app-vault-list-items-container",
templateUrl: "vault-list-items-container.component.html",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private compactModeService = inject(CompactModeService);
Expand Down Expand Up @@ -110,11 +115,51 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
*/
private viewCipherTimeout: number | null;

ciphers = input<PopupCipherView[]>([]);

/**
* The list of ciphers to display.
* If true, we will group ciphers by type (Login, Card, Identity)
* within subheadings in a single container, converted to a WritableSignal.
*/
@Input()
ciphers: PopupCipherView[] = [];
groupByType = input<boolean>(false);

/**
* Computed signal for a grouped list of ciphers with an optional header
*/
cipherGroups$ = computed<
{
subHeaderKey?: string | null;
ciphers: PopupCipherView[];
}[]
>(() => {
const groups: { [key: string]: CipherView[] } = {};

this.ciphers().forEach((cipher) => {
let groupKey;

if (this.groupByType()) {
switch (cipher.type) {
case CipherType.Card:
groupKey = "cards";
break;
case CipherType.Identity:
groupKey = "identities";
break;
}
}

if (!groups[groupKey]) {
groups[groupKey] = [];
}

groups[groupKey].push(cipher);
});

return Object.keys(groups).map((key) => ({
subHeaderKey: this.groupByType ? key : "",
ciphers: groups[key],
}));
});

/**
* Title for the vault list item section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export class VaultPopupItemsService {
map(([hasSearchText, filters]) => {
return hasSearchText || Object.values(filters).some((filter) => filter !== null);
}),
shareReplay({ bufferSize: 1, refCount: true }),
);

/**
Expand Down

0 comments on commit b55468e

Please sign in to comment.