1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-3530] browser extension view cache (#10437)

Introduces a way to store temporary component state, for the purposes of persisting views between extension popup open and close.
This commit is contained in:
Will Martin
2024-08-26 16:09:33 -04:00
committed by GitHub
parent e242d7d2d5
commit 6918606677
9 changed files with 532 additions and 3 deletions

View File

@@ -0,0 +1,83 @@
import { Injector, WritableSignal } from "@angular/core";
import type { FormGroup } from "@angular/forms";
import type { Jsonify, JsonValue } from "type-fest";
type Deserializer<T> = {
/**
* A function to use to safely convert your type from json to your expected type.
*
* @param jsonValue The JSON object representation of your state.
* @returns The fully typed version of your state.
*/
readonly deserializer?: (jsonValue: Jsonify<T>) => T;
};
type BaseCacheOptions<T> = {
/** A unique key for saving the cached value to state */
key: string;
/** An optional injector. Required if the method is called outside of an injection context. */
injector?: Injector;
} & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {
/** The initial value for the signal. */
initialValue: T;
};
/** Extract the value type from a FormGroup */
type FormValue<TFormGroup extends FormGroup> = TFormGroup["value"];
export type FormCacheOptions<TFormGroup extends FormGroup> = BaseCacheOptions<
FormValue<TFormGroup>
> & {
control: TFormGroup;
};
/**
* Cache for temporary component state
*
* #### Implementations
* - browser extension popup: used to persist UI between popup open and close
* - all other clients: noop
*/
export abstract class ViewCacheService {
/**
* Create a signal from a previously cached value. Whenever the signal is updated, the new value is saved to the cache.
*
* Non browser extension implementations are noop and return a normal signal.
*
* @returns the created signal
*
* @example
* ```ts
* const mySignal = this.viewCacheService.signal({
* key: "popup-search-text"
* initialValue: ""
* });
* ```
*/
abstract signal<T>(options: SignalCacheOptions<T>): WritableSignal<T>;
/**
* - Initialize a form from a cached value
* - Save form value to cache when it changes
* - The form is marked dirty if the restored value is not `undefined`.
*
* Non browser extension implementations are noop and return the original form group.
*
* @example
* ```ts
* this.loginDetailsForm = this.viewCacheService.formGroup({
* key: "vault-login-details-form",
* control: this.formBuilder.group({
* username: [""],
* email: [""],
* })
* });
* ```
**/
abstract formGroup<TFormGroup extends FormGroup>(
options: FormCacheOptions<TFormGroup>,
): TFormGroup;
}

View File

@@ -0,0 +1,33 @@
import { Injectable, signal, WritableSignal } from "@angular/core";
import type { FormGroup } from "@angular/forms";
import {
FormCacheOptions,
SignalCacheOptions,
ViewCacheService,
} from "../abstractions/view-cache.service";
/**
* The functionality of the {@link ViewCacheService} is only needed in the browser extension popup,
* yet is provided to all clients to make sharing components easier.
*
* Non-extension clients use this noop implementation.
* */
@Injectable({
providedIn: "root",
})
export class NoopViewCacheService implements ViewCacheService {
/**
* Return a normal signal.
*/
signal<T>(options: SignalCacheOptions<T>): WritableSignal<T> {
return signal(options.initialValue);
}
/**
* Return the original form group.
**/
formGroup<TFormGroup extends FormGroup>(options: FormCacheOptions<TFormGroup>): TFormGroup {
return options.control;
}
}

View File

@@ -268,8 +268,10 @@ import {
} from "@bitwarden/vault-export-core";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
import { NoopViewCacheService } from "../platform/services/noop-view-cache.service";
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
@@ -1290,6 +1292,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultRegistrationFinishService,
deps: [CryptoServiceAbstraction, AccountApiServiceAbstraction],
}),
safeProvider({
provide: ViewCacheService,
useExisting: NoopViewCacheService,
deps: [],
}),
];
@NgModule({