mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-11 13:30:39 +01:00
[AC-2436] Show unassigned items banner in browser (#8656)
* Boostrap basic banner, show for all admins * Remove UI banner, fix method calls * Invert showBanner -> hideBanner * Add api call * Minor tweaks and wording * Change to active user state * Add tests * Fix mixed up names * Simplify logic * Add feature flag * Do not clear on logout * Show banner in browser as well * Update apps/browser/src/_locales/en/messages.json * Update copy --------- Co-authored-by: Addison Beck <github@addisonbeck.com> Co-authored-by: Addison Beck <hello@addisonbeck.com>
This commit is contained in:
parent
4c2afb4121
commit
98ed744ae8
@ -3005,5 +3005,8 @@
|
|||||||
},
|
},
|
||||||
"passkeyRemoved": {
|
"passkeyRemoved": {
|
||||||
"message": "Passkey removed"
|
"message": "Passkey removed"
|
||||||
|
},
|
||||||
|
"unassignedItemsBanner": {
|
||||||
|
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,19 +36,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="loaded">
|
<ng-container *ngIf="loaded">
|
||||||
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
|
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
|
||||||
<app-callout *ngIf="showHowToAutofill" type="info" title="{{ 'howToAutofill' | i18n }}">
|
<app-callout
|
||||||
<p>{{ autofillCalloutText }}</p>
|
*ngIf="
|
||||||
|
(unassignedItemsBannerEnabled$ | async) &&
|
||||||
|
(unassignedItemsBannerService.showBanner$ | async)
|
||||||
|
"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ "unassignedItemsBanner" | i18n }}
|
||||||
|
<a
|
||||||
|
href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console"
|
||||||
|
bitLink
|
||||||
|
linkType="contrast"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>{{ "learnMore" | i18n }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn primary callout-half"
|
class="btn primary callout-half"
|
||||||
appStopClick
|
appStopClick
|
||||||
(click)="dismissCallout()"
|
(click)="unassignedItemsBannerService.hideBanner()"
|
||||||
>
|
>
|
||||||
{{ "gotIt" | i18n }}
|
{{ "gotIt" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn callout-half" appStopClick (click)="goToSettings()">
|
|
||||||
{{ "autofillSettings" | i18n }}
|
|
||||||
</button>
|
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<div class="box list" *ngIf="loginCiphers">
|
<div class="box list" *ngIf="loginCiphers">
|
||||||
<h2 class="box-header">
|
<h2 class="box-header">
|
||||||
|
@ -3,11 +3,14 @@ import { Router } from "@angular/router";
|
|||||||
import { Subject, firstValueFrom, from } from "rxjs";
|
import { Subject, firstValueFrom, from } from "rxjs";
|
||||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@ -54,6 +57,10 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
private loadedTimeout: number;
|
private loadedTimeout: number;
|
||||||
private searchTimeout: number;
|
private searchTimeout: number;
|
||||||
|
|
||||||
|
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.UnassignedItemsBanner,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
@ -70,6 +77,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private vaultFilterService: VaultFilterService,
|
private vaultFilterService: VaultFilterService,
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
protected unassignedItemsBannerService: UnassignedItemsBannerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom, skip } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service";
|
||||||
|
|
||||||
|
describe("UnassignedItemsBanner", () => {
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
|
||||||
|
const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const fakeAccountService = mockAccountServiceWith("userId" as UserId);
|
||||||
|
stateProvider = new FakeStateProvider(fakeAccountService);
|
||||||
|
apiService = mock();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the banner if showBanner local state is true", async () => {
|
||||||
|
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||||
|
showBanner.nextState(true);
|
||||||
|
|
||||||
|
const sut = sutFactory();
|
||||||
|
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
|
||||||
|
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show the banner if showBanner local state is false", async () => {
|
||||||
|
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||||
|
showBanner.nextState(false);
|
||||||
|
|
||||||
|
const sut = sutFactory();
|
||||||
|
expect(await firstValueFrom(sut.showBanner$)).toBe(false);
|
||||||
|
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches from server if local state has not been set yet", async () => {
|
||||||
|
apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||||
|
showBanner.nextState(undefined);
|
||||||
|
|
||||||
|
const sut = sutFactory();
|
||||||
|
// skip first value so we get the recomputed value after the server call
|
||||||
|
expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true);
|
||||||
|
// Expect to have updated local state
|
||||||
|
expect(await firstValueFrom(showBanner.state$)).toBe(true);
|
||||||
|
expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
46
libs/angular/src/services/unassigned-items-banner.service.ts
Normal file
46
libs/angular/src/services/unassigned-items-banner.service.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { EMPTY, concatMap } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import {
|
||||||
|
StateProvider,
|
||||||
|
UNASSIGNED_ITEMS_BANNER_DISK,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>(
|
||||||
|
UNASSIGNED_ITEMS_BANNER_DISK,
|
||||||
|
"showBanner",
|
||||||
|
{
|
||||||
|
deserializer: (b) => b,
|
||||||
|
clearOn: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Displays a banner that tells users how to move their unassigned items into a collection. */
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class UnassignedItemsBannerService {
|
||||||
|
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
|
||||||
|
|
||||||
|
showBanner$ = this._showBanner.state$.pipe(
|
||||||
|
concatMap(async (showBanner) => {
|
||||||
|
// null indicates that the user has not seen or dismissed the banner yet - get the flag from server
|
||||||
|
if (showBanner == null) {
|
||||||
|
const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner();
|
||||||
|
await this._showBanner.update(() => showBannerResponse);
|
||||||
|
return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run
|
||||||
|
}
|
||||||
|
|
||||||
|
return showBanner;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private apiService: ApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async hideBanner() {
|
||||||
|
await this._showBanner.update(() => false);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user