mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
Merge branch 'main' into vault/pm-5273
# Conflicts: # libs/common/src/platform/abstractions/state.service.ts # libs/common/src/state-migrations/migrate.ts
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { fromEvent, map, merge, Observable, of, Subscription, switchMap } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../services/injection-tokens";
|
||||
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class AngularThemingService implements AbstractThemingService {
|
||||
/**
|
||||
* Creates a system theme observable based on watching the given window.
|
||||
* @param window The window that should be watched for system theme changes.
|
||||
* @returns An observable that will track the system theme.
|
||||
*/
|
||||
static createSystemThemeFromWindow(window: Window): Observable<ThemeType> {
|
||||
return merge(
|
||||
// This observable should always emit at least once, so go and get the current system theme designation
|
||||
of(
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light,
|
||||
),
|
||||
// Start listening to changes
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change",
|
||||
).pipe(map((event) => (event.matches ? ThemeType.Dark : ThemeType.Light))),
|
||||
);
|
||||
}
|
||||
|
||||
readonly theme$ = this.themeStateService.selectedTheme$.pipe(
|
||||
switchMap((configuredTheme) => {
|
||||
if (configuredTheme === ThemeType.System) {
|
||||
return this.systemTheme$;
|
||||
}
|
||||
|
||||
return of(configuredTheme);
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
) {}
|
||||
|
||||
applyThemeChangesTo(document: Document): Subscription {
|
||||
return this.theme$.subscribe((theme) => {
|
||||
document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark,
|
||||
);
|
||||
document.documentElement.classList.add("theme_" + theme);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export class ThemeBuilder implements Theme {
|
||||
get effectiveTheme(): ThemeType {
|
||||
return this.configuredTheme != ThemeType.System ? this.configuredTheme : this.systemTheme;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly configuredTheme: ThemeType,
|
||||
readonly systemTheme: ThemeType,
|
||||
) {}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(this.configuredTheme, systemTheme);
|
||||
}
|
||||
|
||||
updateConfiguredTheme(configuredTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(configuredTheme, this.systemTheme);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
export interface Theme {
|
||||
configuredTheme: ThemeType;
|
||||
effectiveTheme: ThemeType;
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
/**
|
||||
* A service for managing and observing the current application theme.
|
||||
*/
|
||||
// FIXME: Rename to ThemingService
|
||||
export abstract class AbstractThemingService {
|
||||
theme$: Observable<Theme>;
|
||||
monitorThemeChanges: () => Promise<void>;
|
||||
updateSystemTheme: (systemTheme: ThemeType) => void;
|
||||
updateConfiguredTheme: (theme: ThemeType) => Promise<void>;
|
||||
/**
|
||||
* The effective theme based on the user configured choice and the current system theme if
|
||||
* the configured choice is {@link ThemeType.System}.
|
||||
*/
|
||||
theme$: Observable<ThemeType>;
|
||||
/**
|
||||
* Listens for effective theme changes and applies changes to the provided document.
|
||||
* @param document The document that should have theme classes applied to it.
|
||||
*
|
||||
* @returns A subscription that can be unsubscribed from to cancel the application of theme classes.
|
||||
*/
|
||||
applyThemeChangesTo: (document: Document) => Subscription;
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
import { ThemeBuilder } from "./theme-builder";
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
|
||||
export class ThemingService implements AbstractThemingService {
|
||||
private _theme = new BehaviorSubject<ThemeBuilder | null>(null);
|
||||
theme$: Observable<Theme> = this._theme.pipe(filter((x) => x !== null));
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private window: Window,
|
||||
private document: Document,
|
||||
) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.monitorThemeChanges();
|
||||
}
|
||||
|
||||
async monitorThemeChanges(): Promise<void> {
|
||||
this._theme.next(
|
||||
new ThemeBuilder(await this.stateService.getTheme(), await this.getSystemTheme()),
|
||||
);
|
||||
this.monitorConfiguredThemeChanges();
|
||||
this.monitorSystemThemeChanges();
|
||||
}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): void {
|
||||
this._theme.next(this._theme.getValue().updateSystemTheme(systemTheme));
|
||||
}
|
||||
|
||||
async updateConfiguredTheme(theme: ThemeType): Promise<void> {
|
||||
await this.stateService.setTheme(theme);
|
||||
this._theme.next(this._theme.getValue().updateConfiguredTheme(theme));
|
||||
}
|
||||
|
||||
protected monitorConfiguredThemeChanges(): void {
|
||||
this.theme$.subscribe((theme: Theme) => {
|
||||
this.document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark,
|
||||
);
|
||||
this.document.documentElement.classList.add("theme_" + theme.effectiveTheme);
|
||||
});
|
||||
}
|
||||
|
||||
// We use a media match query for monitoring the system theme on web and browser, but this doesn't work for electron apps on Linux.
|
||||
// In desktop we override these methods to track systemTheme with the electron renderer instead, which works for all OSs.
|
||||
protected async getSystemTheme(): Promise<ThemeType> {
|
||||
return this.window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
}
|
||||
|
||||
protected monitorSystemThemeChanges(): void {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change",
|
||||
).subscribe((event) => {
|
||||
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { InjectionToken } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
|
||||
declare const tag: unique symbol;
|
||||
@@ -43,3 +45,6 @@ export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promi
|
||||
export const LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
|
||||
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
|
||||
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");
|
||||
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
|
||||
"SYSTEM_THEME_OBSERVABLE",
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { UnwrapOpaque } from "type-fest";
|
||||
|
||||
@@ -160,6 +159,10 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
|
||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
import {
|
||||
DefaultThemeStateService,
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
@@ -231,7 +234,7 @@ import { UnauthGuard } from "../auth/guards/unauth.guard";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
import { ThemingService } from "../platform/services/theming/theming.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";
|
||||
|
||||
@@ -248,6 +251,7 @@ import {
|
||||
STATE_FACTORY,
|
||||
STATE_SERVICE_USE_CACHE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
WINDOW,
|
||||
} from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
@@ -300,6 +304,21 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
provide: LOG_MAC_FAILURES,
|
||||
useValue: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useFactory: (window: Window) => AngularThemingService.createSystemThemeFromWindow(window),
|
||||
deps: [WINDOW],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ThemeStateService,
|
||||
useClass: DefaultThemeStateService,
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AbstractThemingService,
|
||||
useClass: AngularThemingService,
|
||||
deps: [ThemeStateService, SYSTEM_THEME_OBSERVABLE],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AppIdServiceAbstraction,
|
||||
useClass: AppIdService,
|
||||
@@ -774,11 +793,6 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
useClass: TwoFactorService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AbstractThemingService,
|
||||
useClass: ThemingService,
|
||||
deps: [StateServiceAbstraction, WINDOW, DOCUMENT as SafeInjectionToken<Document>],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: FormValidationErrorsServiceAbstraction,
|
||||
useClass: FormValidationErrorsService,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DeviceKey, MasterKey } from "../../types/key";
|
||||
import { KdfType, ThemeType } from "../enums";
|
||||
import { KdfType } from "../enums";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountDecryptionOptions } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
@@ -324,8 +324,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
AbstractMemoryStorageService,
|
||||
AbstractStorageService,
|
||||
} from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType } from "../enums";
|
||||
import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
|
||||
import { StateFactory } from "../factories/state-factory";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
@@ -1663,23 +1663,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getTheme(options?: StorageOptions): Promise<ThemeType> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.theme;
|
||||
}
|
||||
|
||||
async setTheme(value: ThemeType, options?: StorageOptions): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
globals.theme = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
|
||||
@@ -63,6 +63,7 @@ export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||
export const THEMING_DISK = new StateDefinition("theming", "disk");
|
||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
||||
|
||||
// Secrets Manager
|
||||
|
||||
38
libs/common/src/platform/theming/theme-state.service.ts
Normal file
38
libs/common/src/platform/theming/theme-state.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { ThemeType } from "../enums";
|
||||
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "../state";
|
||||
|
||||
export abstract class ThemeStateService {
|
||||
/**
|
||||
* The users selected theme.
|
||||
*/
|
||||
selectedTheme$: Observable<ThemeType>;
|
||||
|
||||
/**
|
||||
* A method for updating the current users configured theme.
|
||||
* @param theme The chosen user theme.
|
||||
*/
|
||||
setSelectedTheme: (theme: ThemeType) => Promise<void>;
|
||||
}
|
||||
|
||||
const THEME_SELECTION = new KeyDefinition<ThemeType>(THEMING_DISK, "selection", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
|
||||
export class DefaultThemeStateService implements ThemeStateService {
|
||||
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
|
||||
|
||||
selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
|
||||
|
||||
constructor(
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private defaultTheme: ThemeType = ThemeType.System,
|
||||
) {}
|
||||
|
||||
async setSelectedTheme(theme: ThemeType): Promise<void> {
|
||||
await this.selectedThemeState.update(() => theme, {
|
||||
shouldUpdate: (currentTheme) => currentTheme !== theme,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-m
|
||||
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
|
||||
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
|
||||
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
|
||||
import { LocalDataMigrator } from "./migrations/35-move-local-data-to-state-provider";
|
||||
import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-state-providers";
|
||||
import { LocalDataMigrator } from "./migrations/36-move-local-data-to-state-provider";
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
@@ -40,7 +41,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 2;
|
||||
export const CURRENT_VERSION = 35;
|
||||
export const CURRENT_VERSION = 36;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -78,7 +79,8 @@ export function createMigrationBuilder() {
|
||||
.with(PreferredLanguageMigrator, 31, 32)
|
||||
.with(AppIdMigrator, 32, 33)
|
||||
.with(DomainSettingsMigrator, 33, 34)
|
||||
.with(LocalDataMigrator, 34, CURRENT_VERSION);
|
||||
.with(MoveThemeToStateProviderMigrator, 34, 35)
|
||||
.with(LocalDataMigrator, 35, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -286,7 +286,11 @@ function expectInjectedData(
|
||||
export async function runMigrator<
|
||||
TMigrator extends Migrator<number, number>,
|
||||
TUsers extends readonly string[] = string[],
|
||||
>(migrator: TMigrator, initalData?: InitialDataHint<TUsers>): Promise<Record<string, unknown>> {
|
||||
>(
|
||||
migrator: TMigrator,
|
||||
initalData?: InitialDataHint<TUsers>,
|
||||
direction: "migrate" | "rollback" = "migrate",
|
||||
): Promise<Record<string, unknown>> {
|
||||
// Inject fake data at every level of the object
|
||||
const allInjectedData = injectData(initalData, []);
|
||||
|
||||
@@ -294,7 +298,11 @@ export async function runMigrator<
|
||||
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
|
||||
|
||||
// Run their migrations
|
||||
await migrator.migrate(helper);
|
||||
if (direction === "rollback") {
|
||||
await migrator.rollback(helper);
|
||||
} else {
|
||||
await migrator.migrate(helper);
|
||||
}
|
||||
const [data, leftoverInjectedData] = expectInjectedData(
|
||||
fakeStorageService.internalStore,
|
||||
allInjectedData,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MoveThemeToStateProviderMigrator } from "./35-move-theme-to-state-providers";
|
||||
|
||||
describe("MoveThemeToStateProviders", () => {
|
||||
const sut = new MoveThemeToStateProviderMigrator(34, 35);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("migrates global theme and deletes it", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
global: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global_theming_selection: "dark",
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([{}, null])(
|
||||
"doesn't touch it if global state looks like: '%s'",
|
||||
async (globalState) => {
|
||||
const output = await runMigrator(sut, {
|
||||
global: globalState,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global: globalState,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
it("migrates state provider theme back to original location when no global", async () => {
|
||||
const output = await runMigrator(
|
||||
sut,
|
||||
{
|
||||
global_theming_selection: "disk",
|
||||
},
|
||||
"rollback",
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
theme: "disk",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates state provider theme back to legacy location when there is an existing global object", async () => {
|
||||
const output = await runMigrator(
|
||||
sut,
|
||||
{
|
||||
global_theming_selection: "disk",
|
||||
global: {
|
||||
other: "stuff",
|
||||
},
|
||||
},
|
||||
"rollback",
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
theme: "disk",
|
||||
other: "stuff",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does nothing if no theme in state provider location", async () => {
|
||||
const output = await runMigrator(sut, {}, "rollback");
|
||||
expect(output).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobal = { theme?: string };
|
||||
|
||||
const THEME_SELECTION: KeyDefinitionLike = {
|
||||
key: "selection",
|
||||
stateDefinition: { name: "theming" },
|
||||
};
|
||||
|
||||
export class MoveThemeToStateProviderMigrator extends Migrator<34, 35> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobalState = await helper.get<ExpectedGlobal>("global");
|
||||
const theme = legacyGlobalState?.theme;
|
||||
if (theme != null) {
|
||||
await helper.setToGlobal(THEME_SELECTION, theme);
|
||||
delete legacyGlobalState.theme;
|
||||
await helper.set("global", legacyGlobalState);
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const theme = await helper.getFromGlobal<string>(THEME_SELECTION);
|
||||
if (theme != null) {
|
||||
const legacyGlobal = (await helper.get<ExpectedGlobal>("global")) ?? {};
|
||||
legacyGlobal.theme = theme;
|
||||
await helper.set("global", legacyGlobal);
|
||||
await helper.removeFromGlobal(THEME_SELECTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { MockProxy } from "jest-mock-extended";
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { LocalDataMigrator } from "./35-move-local-data-to-state-provider";
|
||||
import { LocalDataMigrator } from "./36-move-local-data-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
@@ -80,8 +80,8 @@ describe("LocalDataMigrator", () => {
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 35);
|
||||
sut = new LocalDataMigrator(34, 35);
|
||||
helper = mockMigrationHelper(exampleJSON(), 36);
|
||||
sut = new LocalDataMigrator(35, 36);
|
||||
});
|
||||
|
||||
it("should remove local data from all accounts", async () => {
|
||||
@@ -105,8 +105,8 @@ describe("LocalDataMigrator", () => {
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 35);
|
||||
sut = new LocalDataMigrator(34, 35);
|
||||
helper = mockMigrationHelper(rollbackJSON(), 36);
|
||||
sut = new LocalDataMigrator(35, 36);
|
||||
});
|
||||
|
||||
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||
@@ -17,7 +17,7 @@ const CIPHERS_DISK: KeyDefinitionLike = {
|
||||
},
|
||||
};
|
||||
|
||||
export class LocalDataMigrator extends Migrator<34, 35> {
|
||||
export class LocalDataMigrator extends Migrator<35, 36> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
Reference in New Issue
Block a user