mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
[PM-9111] Extension: persist add/edit form (#12236)
* remove todo * Retrieve cache cipher for add-edit form * user prefilled cipher for add-edit form * add listener for clearing view cache * clear local cache when clearing global state * track initial value of cache for down stream logic that should only occur on non-cached values * add feature flag for edit form persistence * add tests for cipher form cache service * fix optional initialValues * add services to cipher form storybook * fix strict types * rename variables to be platform agnostic * use deconstructed collectionIds variable to avoid them be overwritten * use the originalCipherView for initial values * add comment about signal equality * prevent events from being emitted when adding uris to the existing form - This stops other values from being overwrote in the initialization process * add check for cached cipher when adding initial uris
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { CipherFormCacheService } from "./default-cipher-form-cache.service";
|
||||
|
||||
describe("CipherFormCacheService", () => {
|
||||
let service: CipherFormCacheService;
|
||||
let testBed: TestBed;
|
||||
const cacheSignal = signal<CipherView | null>(null);
|
||||
const getCacheSignal = jest.fn().mockReturnValue(cacheSignal);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const cacheSetMock = jest.spyOn(cacheSignal, "set");
|
||||
|
||||
beforeEach(() => {
|
||||
getCacheSignal.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
cacheSetMock.mockClear();
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ViewCacheService, useValue: { signal: getCacheSignal } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
CipherFormCacheService,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature enabled", () => {
|
||||
beforeEach(async () => {
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("`getCachedCipherView` returns the cipher", async () => {
|
||||
cacheSignal.set({ id: "cipher-4" } as CipherView);
|
||||
service = testBed.inject(CipherFormCacheService);
|
||||
await service.init();
|
||||
|
||||
expect(service.getCachedCipherView()).toEqual({ id: "cipher-4" });
|
||||
});
|
||||
|
||||
it("updates the signal value", async () => {
|
||||
service = testBed.inject(CipherFormCacheService);
|
||||
await service.init();
|
||||
|
||||
service.cacheCipherView({ id: "cipher-5" } as CipherView);
|
||||
|
||||
expect(cacheSignal.set).toHaveBeenCalledWith({ id: "cipher-5" });
|
||||
});
|
||||
|
||||
describe("initializedWithValue", () => {
|
||||
it("sets `initializedWithValue` to true when there is a cached cipher", async () => {
|
||||
cacheSignal.set({ id: "cipher-3" } as CipherView);
|
||||
service = testBed.inject(CipherFormCacheService);
|
||||
await service.init();
|
||||
|
||||
expect(service.initializedWithValue).toBe(true);
|
||||
});
|
||||
|
||||
it("sets `initializedWithValue` to false when there is not a cached cipher", async () => {
|
||||
cacheSignal.set(null);
|
||||
service = testBed.inject(CipherFormCacheService);
|
||||
await service.init();
|
||||
|
||||
expect(service.initializedWithValue).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("featured disabled", () => {
|
||||
beforeEach(async () => {
|
||||
cacheSignal.set({ id: "cipher-1" } as CipherView);
|
||||
getFeatureFlag.mockResolvedValue(false);
|
||||
cacheSetMock.mockClear();
|
||||
|
||||
service = testBed.inject(CipherFormCacheService);
|
||||
await service.init();
|
||||
});
|
||||
|
||||
it("sets `initializedWithValue` to false", () => {
|
||||
expect(service.initializedWithValue).toBe(false);
|
||||
});
|
||||
|
||||
it("`getCachedCipherView` returns null", () => {
|
||||
expect(service.getCachedCipherView()).toBeNull();
|
||||
});
|
||||
|
||||
it("does not update the signal value", () => {
|
||||
service.cacheCipherView({ id: "cipher-2" } as CipherView);
|
||||
|
||||
expect(cacheSignal.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
const CIPHER_FORM_CACHE_KEY = "cipher-form-cache";
|
||||
|
||||
@Injectable()
|
||||
export class CipherFormCacheService {
|
||||
private viewCacheService: ViewCacheService = inject(ViewCacheService);
|
||||
private configService: ConfigService = inject(ConfigService);
|
||||
|
||||
/** True when the `PM9111ExtensionPersistAddEditForm` flag is enabled */
|
||||
private featureEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* When true the `CipherFormCacheService` a cipher was stored in cache when the service was initialized.
|
||||
* Otherwise false, when the cache was empty.
|
||||
*
|
||||
* This is helpful to know the initial state of the cache as it can be populated quickly after initialization.
|
||||
*/
|
||||
initializedWithValue: boolean;
|
||||
|
||||
private cipherCache = this.viewCacheService.signal<CipherView | null>({
|
||||
key: CIPHER_FORM_CACHE_KEY,
|
||||
initialValue: null,
|
||||
deserializer: CipherView.fromJSON,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.initializedWithValue = !!this.cipherCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called once before interacting with the cached cipher, otherwise methods will be noop.
|
||||
*/
|
||||
async init() {
|
||||
this.featureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM9111ExtensionPersistAddEditForm,
|
||||
);
|
||||
|
||||
if (!this.featureEnabled) {
|
||||
this.initializedWithValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cache with the new CipherView.
|
||||
*/
|
||||
cacheCipherView(cipherView: CipherView): void {
|
||||
if (!this.featureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new shallow reference to force the signal to update
|
||||
// By default, signals use `Object.is` to determine equality
|
||||
// Docs: https://angular.dev/guide/signals#signal-equality-functions
|
||||
this.cipherCache.set({ ...cipherView } as CipherView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached CipherView when available.
|
||||
*/
|
||||
getCachedCipherView(): CipherView | null {
|
||||
if (!this.featureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.cipherCache();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user