Skip to content

Commit

Permalink
Merge branch 'main' into pm-15814-alert-owners-of-reseller-managed-or…
Browse files Browse the repository at this point in the history
…gs-to-renewal
  • Loading branch information
cyprain-okeke authored Jan 13, 2025
2 parents c1f8c39 + 52b6bfe commit 1e002b1
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,76 @@
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
<div class="tw-ml-1">
<bit-section-header>
<h2 bitTypography="h6">
{{ title }}
</h2>
<button
*ngIf="showRefresh"
bitIconButton="bwi-refresh"
type="button"
size="small"
(click)="onRefresh.emit()"
[appA11yTitle]="'refresh' | i18n"
></button>
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
</bit-section-header>
</div>
<ng-container *ngIf="collapsibleKey">
<button
class="tw-group/vault-section-header hover:tw-bg-secondary-100 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-rounded-md focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
[ngClass]="{
'tw-border-b-secondary-300': !sectionOpenState(),
'tw-border-b-transparent': sectionOpenState(),
}"
type="button"
[bitDisclosureTriggerFor]="disclosureRef"
(click)="toggleSectionOpen()"
>
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
</button>
<ng-container *ngTemplateOutlet="descriptionText"></ng-container>
<bit-disclosure #disclosureRef [open]="sectionOpenState()" (openChange)="rerenderViewport()">
<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>
</div>
<ng-container *ngTemplateOutlet="descriptionText"></ng-container>
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
</ng-container>
</bit-section>

<ng-template #sectionHeader>
<bit-section-header class="tw-p-0.5 -tw-mt-0.5 -tw-mx-0.5">
<h2 bitTypography="h6">
{{ title }}
</h2>
<button
*ngIf="showRefresh"
bitIconButton="bwi-refresh"
type="button"
size="small"
(click)="onRefresh.emit()"
[appA11yTitle]="'refresh' | i18n"
></button>
<span bitTypography="body2" slot="end">
<span
[ngClass]="{
'group-hover/vault-section-header:tw-hidden group-focus-visible/vault-section-header:tw-hidden':
collapsibleKey && sectionOpenState(),
'tw-hidden': collapsibleKey && !sectionOpenState(),
}"
>
{{ ciphers.length }}
</span>
<span class="tw-pr-1" *ngIf="collapsibleKey">
<i
class="bwi"
[ngClass]="{
'bwi-angle-down tw-inline-block': !sectionOpenState(),
'bwi-angle-up tw-hidden group-hover/vault-section-header:tw-inline-block group-focus-visible/vault-section-header:tw-inline-block':
sectionOpenState(),
}"
aria-hidden="true"
></i>
</span>
</span>
</bit-section-header>
</ng-template>

<ng-template #descriptionText>
<div *ngIf="description" class="tw-text-muted tw-px-1 tw-mb-2" bitTypography="body2">
{{ description }}
</div>
</ng-template>

<ng-template #itemGroup>
<bit-item-group>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
Expand Down Expand Up @@ -85,4 +138,4 @@ <h2 bitTypography="h6">
</bit-item>
</cdk-virtual-scroll-viewport>
</bit-item-group>
</bit-section>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CdkVirtualScrollViewport, ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import {
AfterViewInit,
Expand All @@ -9,8 +9,11 @@ import {
EventEmitter,
inject,
Input,
OnInit,
Output,
Signal,
signal,
ViewChild,
} from "@angular/core";
import { Router, RouterLink } from "@angular/router";
import { map } from "rxjs";
Expand All @@ -25,6 +28,8 @@ import {
BadgeModule,
ButtonModule,
CompactModeService,
DisclosureComponent,
DisclosureTriggerForDirective,
DialogService,
IconButtonModule,
ItemModule,
Expand All @@ -41,6 +46,7 @@ import {
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { VaultPopupSectionService } from "../../../services/vault-popup-section.service";
import { PopupCipherView } from "../../../views/popup-cipher.view";
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
Expand All @@ -61,14 +67,25 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
ItemMoreOptionsComponent,
OrgIconDirective,
ScrollingModule,
DisclosureComponent,
DisclosureTriggerForDirective,
DecryptionFailureDialogComponent,
],
selector: "app-vault-list-items-container",
templateUrl: "vault-list-items-container.component.html",
standalone: true,
})
export class VaultListItemsContainerComponent implements AfterViewInit {
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private compactModeService = inject(CompactModeService);
private vaultPopupSectionService = inject(VaultPopupSectionService);

@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort: CdkVirtualScrollViewport;
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;

/**
* Indicates whether the section should be open or closed if collapsibleKey is provided
*/
protected sectionOpenState: Signal<boolean> | undefined;

/**
* The class used to set the height of a bit item's inner content.
Expand Down Expand Up @@ -106,6 +123,15 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
@Input()
title: string;

/**
* Optionally allow the items to be collapsed.
*
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
* collapsed state is stored locally.
*/
@Input()
collapsibleKey: "favorites" | "allItems" | undefined;

/**
* Optional description for the vault list item section. Will be shown below the title even when
* no ciphers are available.
Expand Down Expand Up @@ -168,6 +194,16 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
private dialogService: DialogService,
) {}

ngOnInit(): void {
if (!this.collapsibleKey) {
return;
}

this.sectionOpenState = this.vaultPopupSectionService.getOpenDisplayStateForSection(
this.collapsibleKey,
);
}

async ngAfterViewInit() {
const autofillShortcut = await this.platformUtilsService.getAutofillKeyboardShortcut();

Expand Down Expand Up @@ -239,4 +275,30 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
cipher.canLaunch ? 200 : 0,
);
}

/**
* Update section open/close state based on user action
*/
async toggleSectionOpen() {
if (!this.collapsibleKey) {
return;
}

await this.vaultPopupSectionService.updateSectionOpenStoredState(
this.collapsibleKey,
this.disclosure.open,
);
}

/**
* Force virtual scroll to update its viewport size to avoid display bugs
*
* Angular CDK scroll has a bug when used with conditional rendering:
* https://github.com/angular/components/issues/24362
*/
protected rerenderViewport() {
setTimeout(() => {
this.viewPort.checkViewportSize();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@
[title]="'favorites' | i18n"
[ciphers]="favoriteCiphers$ | async"
id="favorites"
collapsibleKey="favorites"
></app-vault-list-items-container>
<app-vault-list-items-container
[title]="'allItems' | i18n"
[ciphers]="remainingCiphers$ | async"
id="allItems"
disableSectionMargin
collapsibleKey="allItems"
></app-vault-list-items-container>
</div>
</ng-container>
Expand Down
129 changes: 129 additions & 0 deletions apps/browser/src/vault/popup/services/vault-popup-section.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { computed, effect, inject, Injectable, signal, Signal } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { map } from "rxjs";

import {
KeyDefinition,
StateProvider,
VAULT_SETTINGS_DISK,
} from "@bitwarden/common/platform/state";

import { VaultPopupItemsService } from "./vault-popup-items.service";

export type PopupSectionOpen = {
favorites: boolean;
allItems: boolean;
};

const SECTION_OPEN_KEY = new KeyDefinition<PopupSectionOpen>(VAULT_SETTINGS_DISK, "sectionOpen", {
deserializer: (obj) => obj,
});

const INITIAL_OPEN: PopupSectionOpen = {
favorites: true,
allItems: true,
};

@Injectable({
providedIn: "root",
})
export class VaultPopupSectionService {
private vaultPopupItemsService = inject(VaultPopupItemsService);
private stateProvider = inject(StateProvider);

private hasFilterOrSearchApplied = toSignal(
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)),
);

/**
* Used to change the open/close state without persisting it to the local disk. Reflects
* application-applied overrides.
* `null` means there is no current override
*/
private temporaryStateOverride = signal<Partial<PopupSectionOpen> | null>(null);

constructor() {
effect(
() => {
/**
* auto-open all sections when search or filter is applied, and remove
* override when search or filter is removed
*/
if (this.hasFilterOrSearchApplied()) {
this.temporaryStateOverride.set(INITIAL_OPEN);
} else {
this.temporaryStateOverride.set(null);
}
},
{
allowSignalWrites: true,
},
);
}

/**
* Stored disk state for the open/close state of the sections. Will be `null` if user has never
* opened/closed a section
*/
private sectionOpenStateProvider = this.stateProvider.getGlobal(SECTION_OPEN_KEY);

/**
* Stored disk state for the open/close state of the sections, with an initial value provided
* if the stored disk state does not yet exist.
*/
private sectionOpenStoredState = toSignal<PopupSectionOpen | null>(
this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)),
// Indicates that the state value is loading
{ initialValue: null },
);

/**
* Indicates the current open/close display state of each section, accounting for temporary
* non-persisted overrides.
*/
sectionOpenDisplayState: Signal<Partial<PopupSectionOpen>> = computed(() => ({
...this.sectionOpenStoredState(),
...this.temporaryStateOverride(),
}));

/**
* Retrieve the open/close display state for a given section.
*
* @param sectionKey section key
*/
getOpenDisplayStateForSection(sectionKey: keyof PopupSectionOpen): Signal<boolean | undefined> {
return computed(() => this.sectionOpenDisplayState()?.[sectionKey]);
}

/**
* Updates the stored open/close state of a given section. Should be called only when a user action
* is taken directly to change the open/close state.
*
* Removes any current temporary override for the given section, as direct user action should
* supersede any application-applied overrides.
*
* @param sectionKey section key
*/
async updateSectionOpenStoredState(
sectionKey: keyof PopupSectionOpen,
open: boolean,
): Promise<void> {
await this.sectionOpenStateProvider.update((currentState) => {
return {
...(currentState ?? INITIAL_OPEN),
[sectionKey]: open,
};
});

this.temporaryStateOverride.update((prev) => {
if (prev !== null) {
return {
...prev,
[sectionKey]: open,
};
}

return prev;
});
}
}
21 changes: 21 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@
"atRiskMembers": {
"message": "At-risk members"
},
"atRiskMembersWithCount": {
"message": "At-risk members ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
},
"atRiskMembersDescriptionWithApp": {
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
"placeholders": {
"appname": {
"content": "$1",
"example": "Salesforce"
}
}
},
"totalMembers": {
"message": "Total members"
},
Expand Down
Loading

0 comments on commit 1e002b1

Please sign in to comment.