[PM-7713] Refresh Appearance Settings (#10458)

* add v2 of appearance component

* swap in new appearance component based on refresh flag

* update default theme verbiage
This commit is contained in:
Nick Krantz 2024-08-08 17:01:47 -05:00 committed by GitHub
parent 4556e59ee8
commit d212bb1fd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 261 additions and 3 deletions

View File

@ -4164,5 +4164,11 @@
},
"accountActions": {
"message": "Account actions"
},
"showNumberOfAutofillSuggestions": {
"message": "Show number of login autofill suggestions on extension icon"
},
"systemDefault": {
"message": "System default"
}
}

View File

@ -79,6 +79,7 @@ import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
@ -339,12 +340,11 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "premium" },
},
{
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
path: "appearance",
component: AppearanceComponent,
canActivate: [authGuard],
data: { state: "appearance" },
},
}),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
path: "clone-cipher",
canActivate: [authGuard],

View File

@ -0,0 +1,32 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'appearance' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<form [formGroup]="appearanceForm">
<bit-card>
<bit-form-field>
<bit-label>{{ "theme" | i18n }}</bit-label>
<bit-select formControlName="theme">
<bit-option
*ngFor="let o of themeOptions"
[value]="o.value"
[label]="o.name"
></bit-option>
</bit-select>
</bit-form-field>
<bit-form-control>
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
<bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control disableMargin>
<input bitCheckbox formControlName="enableFavicon" type="checkbox" />
<bit-label>{{ "enableFavicon" | i18n }}</bit-label>
</bit-form-control>
</bit-card>
</form>
</popup-page>

View File

@ -0,0 +1,110 @@
import { Component, Input } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { AppearanceV2Component } from "./appearance-v2.component";
@Component({
standalone: true,
selector: "popup-header",
template: `<ng-content></ng-content>`,
})
class MockPopupHeaderComponent {
@Input() pageTitle: string;
@Input() backAction: () => void;
}
@Component({
standalone: true,
selector: "popup-page",
template: `<ng-content></ng-content>`,
})
class MockPopupPageComponent {}
describe("AppearanceV2Component", () => {
let component: AppearanceV2Component;
let fixture: ComponentFixture<AppearanceV2Component>;
const showFavicons$ = new BehaviorSubject<boolean>(true);
const enableBadgeCounter$ = new BehaviorSubject<boolean>(true);
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
beforeEach(async () => {
setSelectedTheme.mockClear();
setShowFavicons.mockClear();
setEnableBadgeCounter.mockClear();
await TestBed.configureTestingModule({
imports: [AppearanceV2Component],
providers: [
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: MessagingService, useValue: mock<MessagingService>() },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: DomainSettingsService, useValue: { showFavicons$, setShowFavicons } },
{
provide: BadgeSettingsServiceAbstraction,
useValue: { enableBadgeCounter$, setEnableBadgeCounter },
},
{ provide: ThemeStateService, useValue: { selectedTheme$, setSelectedTheme } },
],
})
.overrideComponent(AppearanceV2Component, {
remove: {
imports: [PopupHeaderComponent, PopupPageComponent],
},
add: {
imports: [MockPopupHeaderComponent, MockPopupPageComponent],
},
})
.compileComponents();
fixture = TestBed.createComponent(AppearanceV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("populates the form with the user's current settings", () => {
expect(component.appearanceForm.value).toEqual({
enableFavicon: true,
enableBadgeCounter: true,
theme: ThemeType.Nord,
});
});
describe("form changes", () => {
it("updates the users theme", () => {
component.appearanceForm.controls.theme.setValue(ThemeType.Light);
expect(setSelectedTheme).toHaveBeenCalledWith(ThemeType.Light);
});
it("updates the users favicon setting", () => {
component.appearanceForm.controls.enableFavicon.setValue(false);
expect(setShowFavicons).toHaveBeenCalledWith(false);
});
it("updates the users badge counter setting", () => {
component.appearanceForm.controls.enableBadgeCounter.setValue(false);
expect(setEnableBadgeCounter).toHaveBeenCalledWith(false);
});
});
});

View File

@ -0,0 +1,110 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CheckboxModule } from "@bitwarden/components";
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@Component({
standalone: true,
templateUrl: "./appearance-v2.component.html",
imports: [
CommonModule,
JslibModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
CardComponent,
FormFieldModule,
SelectModule,
ReactiveFormsModule,
CheckboxModule,
],
})
export class AppearanceV2Component implements OnInit {
appearanceForm = this.formBuilder.group({
enableFavicon: false,
enableBadgeCounter: true,
theme: ThemeType.System,
});
/** Available theme options */
themeOptions: { name: string; value: ThemeType }[];
constructor(
private messagingService: MessagingService,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
private themeStateService: ThemeStateService,
private formBuilder: FormBuilder,
private destroyRef: DestroyRef,
i18nService: I18nService,
) {
this.themeOptions = [
{ name: i18nService.t("systemDefault"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
}
async ngOnInit() {
const enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
const enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
const theme = await firstValueFrom(this.themeStateService.selectedTheme$);
// Set initial values for the form
this.appearanceForm.setValue({
enableFavicon,
enableBadgeCounter,
theme,
});
this.appearanceForm.controls.theme.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((newTheme) => {
void this.saveTheme(newTheme);
});
this.appearanceForm.controls.enableFavicon.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((enableFavicon) => {
void this.updateFavicon(enableFavicon);
});
this.appearanceForm.controls.enableBadgeCounter.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((enableBadgeCounter) => {
void this.updateBadgeCounter(enableBadgeCounter);
});
}
async updateFavicon(enableFavicon: boolean) {
await this.domainSettingsService.setShowFavicons(enableFavicon);
}
async updateBadgeCounter(enableBadgeCounter: boolean) {
await this.badgeSettingsService.setEnableBadgeCounter(enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async saveTheme(newTheme: ThemeType) {
await this.themeStateService.setSelectedTheme(newTheme);
}
}