Do not redirect after saving changes to excluded domains (#11676)

This commit is contained in:
Jonathan Prusik 2024-10-24 10:15:24 -04:00 committed by GitHub
parent b3b311e164
commit 15c301d39f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 61 additions and 30 deletions

View File

@ -11,7 +11,7 @@
accountSwitcherEnabled ? ("excludedDomainsDescAlt" | i18n) : ("excludedDomainsDesc" | i18n) accountSwitcherEnabled ? ("excludedDomainsDescAlt" | i18n) : ("excludedDomainsDesc" | i18n)
}} }}
</p> </p>
<bit-section> <bit-section *ngIf="!isLoading">
<bit-section-header> <bit-section-header>
<h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2> <h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2>
<span bitTypography="body2" slot="end">{{ excludedDomainsState?.length || 0 }}</span> <span bitTypography="body2" slot="end">{{ excludedDomainsState?.length || 0 }}</span>
@ -57,7 +57,13 @@
</bit-section> </bit-section>
</div> </div>
<popup-footer slot="footer"> <popup-footer slot="footer">
<button bitButton buttonType="primary" type="submit" (click)="saveChanges()"> <button
bitButton
buttonType="primary"
type="submit"
[disabled]="dataIsPristine"
(click)="saveChanges()"
>
{{ "save" | i18n }} {{ "save" | i18n }}
</button> </button>
</popup-footer> </popup-footer>

View File

@ -1,13 +1,19 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { QueryList, Component, ElementRef, OnDestroy, OnInit, ViewChildren } from "@angular/core"; import {
QueryList,
Component,
ElementRef,
OnDestroy,
AfterViewInit,
ViewChildren,
} from "@angular/core";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { Router, RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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";
@ -29,8 +35,6 @@ import { PopupFooterComponent } from "../../../platform/popup/layout/popup-foote
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
const BroadcasterSubscriptionId = "excludedDomainsState";
@Component({ @Component({
selector: "app-excluded-domains", selector: "app-excluded-domains",
templateUrl: "excluded-domains.component.html", templateUrl: "excluded-domains.component.html",
@ -55,11 +59,12 @@ const BroadcasterSubscriptionId = "excludedDomainsState";
TypographyModule, TypographyModule,
], ],
}) })
export class ExcludedDomainsComponent implements OnInit, OnDestroy { export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>>; @ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>>;
accountSwitcherEnabled = false; accountSwitcherEnabled = false;
dataIsPristine = true; dataIsPristine = true;
isLoading = false;
excludedDomainsState: string[] = []; excludedDomainsState: string[] = [];
storedExcludedDomains: string[] = []; storedExcludedDomains: string[] = [];
// How many fields should be non-editable before editable fields // How many fields should be non-editable before editable fields
@ -70,16 +75,27 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
constructor( constructor(
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
private i18nService: I18nService, private i18nService: I18nService,
private router: Router,
private broadcasterService: BroadcasterService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
) { ) {
this.accountSwitcherEnabled = enableAccountSwitching(); this.accountSwitcherEnabled = enableAccountSwitching();
} }
async ngOnInit() { async ngAfterViewInit() {
const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); this.domainSettingsService.neverDomains$
.pipe(takeUntil(this.destroy$))
.subscribe((neverDomains: NeverDomains) => this.handleStateUpdate(neverDomains));
this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
this.focusNewUriInput(last);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
handleStateUpdate(neverDomains: NeverDomains) {
if (neverDomains) { if (neverDomains) {
this.storedExcludedDomains = Object.keys(neverDomains); this.storedExcludedDomains = Object.keys(neverDomains);
} }
@ -89,15 +105,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
// Do not allow the first x (pre-existing) fields to be edited // Do not allow the first x (pre-existing) fields to be edited
this.fieldsEditThreshold = this.storedExcludedDomains.length; this.fieldsEditThreshold = this.storedExcludedDomains.length;
this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => { this.dataIsPristine = true;
this.focusNewUriInput(last); this.isLoading = false;
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.complete();
} }
focusNewUriInput(elementRef: ElementRef) { focusNewUriInput(elementRef: ElementRef) {
@ -116,7 +125,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
async removeDomain(i: number) { async removeDomain(i: number) {
this.excludedDomainsState.splice(i, 1); this.excludedDomainsState.splice(i, 1);
// if a pre-existing field was dropped, lower the edit threshold // If a pre-existing field was dropped, lower the edit threshold
if (i < this.fieldsEditThreshold) { if (i < this.fieldsEditThreshold) {
this.fieldsEditThreshold--; this.fieldsEditThreshold--;
} }
@ -132,11 +141,11 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
async saveChanges() { async saveChanges() {
if (this.dataIsPristine) { if (this.dataIsPristine) {
await this.router.navigate(["/notifications"]);
return; return;
} }
this.isLoading = true;
const newExcludedDomainsSaveState: NeverDomains = {}; const newExcludedDomainsSaveState: NeverDomains = {};
const uniqueExcludedDomains = new Set(this.excludedDomainsState); const uniqueExcludedDomains = new Set(this.excludedDomainsState);
@ -151,6 +160,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
this.i18nService.t("excludedDomainsInvalidDomain", uri), this.i18nService.t("excludedDomainsInvalidDomain", uri),
); );
// Don't reset via `handleStateUpdate` to allow existing input value correction
this.isLoading = false;
return; return;
} }
@ -159,7 +170,23 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
try { try {
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState); const existingState = new Set(this.storedExcludedDomains);
const newState = new Set(Object.keys(newExcludedDomainsSaveState));
const stateIsUnchanged =
existingState.size === newState.size &&
new Set([...existingState, ...newState]).size === existingState.size;
// The subscriber updates don't trigger if `setNeverDomains` sets an equivalent state
if (stateIsUnchanged) {
// Reset UI state directly
const constructedNeverDomainsState = this.storedExcludedDomains.reduce(
(neverDomains, uri) => ({ ...neverDomains, [uri]: null }),
{},
);
this.handleStateUpdate(constructedNeverDomainsState);
} else {
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
}
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
@ -169,11 +196,9 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} catch { } catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
// Do not navigate on error // Don't reset via `handleStateUpdate` to preserve input values
return; this.isLoading = false;
} }
await this.router.navigate(["/notifications"]);
} }
trackByFunction(index: number, _: string) { trackByFunction(index: number, _: string) {