mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-06 23:51:28 +01:00
[PM-17186] - Add Card and Identity sub-headers to Autofill Suggestions (#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
This commit is contained in:
parent
2c367444ff
commit
b55468e6a1
@ -7,4 +7,5 @@
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
|
||||
showAutofillButton
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView"
|
||||
[groupByType]="groupByType()"
|
||||
></app-vault-list-items-container>
|
||||
|
@ -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";
|
||||
@ -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
|
||||
|
@ -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"
|
||||
@ -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>
|
||||
@ -48,7 +52,7 @@
|
||||
'tw-hidden': collapsibleKey && !sectionOpenState(),
|
||||
}"
|
||||
>
|
||||
{{ ciphers.length }}
|
||||
{{ ciphers().length }}
|
||||
</span>
|
||||
<span class="tw-pr-1" *ngIf="collapsibleKey">
|
||||
<i
|
||||
@ -73,69 +77,78 @@
|
||||
|
||||
<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>
|
||||
|
@ -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";
|
||||
@ -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,
|
||||
@ -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);
|
||||
@ -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.
|
||||
|
@ -214,6 +214,7 @@ export class VaultPopupItemsService {
|
||||
map(([hasSearchText, filters]) => {
|
||||
return hasSearchText || Object.values(filters).some((filter) => filter !== null);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user