diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index f580b75f1ba..5d48cdabc93 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -43,6 +43,7 @@ (onUpdated)="generate('password settings')" /> this.logService.error(e)); } }); }); @@ -495,7 +509,11 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { * @param requestor a label used to trace generation request * origin in the debugger. */ - protected generate(requestor: string) { + protected async generate(requestor: string) { + if (this.passphraseSettings) { + await this.passphraseSettings.reloadSettings("credential generator"); + } + this.generate$.next(requestor); } diff --git a/libs/tools/generator/components/src/passphrase-settings.component.html b/libs/tools/generator/components/src/passphrase-settings.component.html index d089de7a07b..e6a6815a9f5 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.html +++ b/libs/tools/generator/components/src/passphrase-settings.component.html @@ -7,7 +7,13 @@
{{ "options" | i18n }}
{{ "numWords" | i18n }} - + {{ numWordsBoundariesHint$ | async }} @@ -16,7 +22,13 @@
{{ "options" | i18n }}
{{ "wordSeparator" | i18n }} - + diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index d65e897f4e1..69421d14b38 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,7 +1,19 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs"; +import { + BehaviorSubject, + skip, + takeUntil, + Subject, + filter, + map, + withLatestFrom, + Observable, + merge, + firstValueFrom, + ReplaySubject, +} from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -72,6 +84,12 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { async ngOnInit() { const singleUserId$ = this.singleUserId$(); const settings = await this.generatorService.settings(Generators.passphrase, { singleUserId$ }); + settings + .pipe( + filter((s) => !!s), + takeUntil(this.destroyed$), + ) + .subscribe(this.okSettings$); // skips reactive event emissions to break a subscription cycle settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => { @@ -110,12 +128,61 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { }); // now that outputs are set up, connect inputs - this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings); + this.settings$().pipe(takeUntil(this.destroyed$)).subscribe(settings); + } + + protected settings$(): Observable> { + // save valid changes + const validChanges$ = this.settings.statusChanges.pipe( + filter((status) => status === "VALID"), + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => settings), + ); + + // discards changes but keep the override setting that changed + const overrides = [Controls.capitalize, Controls.includeNumber]; + const overrideChanges$ = this.settings.valueChanges.pipe( + filter((settings) => !!settings), + withLatestFrom(this.okSettings$), + filter(([current, ok]) => overrides.some((c) => (current[c] ?? ok[c]) !== ok[c])), + map(([current, ok]) => { + const copy = { ...ok }; + for (const override of overrides) { + copy[override] = current[override]; + } + return copy; + }), + ); + + // save reloaded settings when requested + const reloadChanges$ = this.reloadSettings$.pipe( + withLatestFrom(this.okSettings$), + map(([, settings]) => settings), + ); + + return merge(validChanges$, overrideChanges$, reloadChanges$); } /** display binding for enterprise policy notice */ protected policyInEffect: boolean; + private okSettings$ = new ReplaySubject(1); + + private reloadSettings$ = new Subject(); + + /** triggers a reload of the users' settings + * @param site labels the invocation site so that an operation + * can be traced back to its origin. Useful for debugging rxjs. + * @returns a promise that completes once a reload occurs. + */ + async reloadSettings(site: string = "component api call") { + const reloadComplete = firstValueFrom(this.okSettings$); + if (this.settings.invalid) { + this.reloadSettings$.next(site); + await reloadComplete; + } + } + private numWordsBoundariesHint = new ReplaySubject(1); /** display binding for min/max constraints of `numWords` */ @@ -144,6 +211,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/util.ts b/libs/tools/generator/components/src/util.ts index d6cd4e6fbaf..7977f774594 100644 --- a/libs/tools/generator/components/src/util.ts +++ b/libs/tools/generator/components/src/util.ts @@ -49,7 +49,7 @@ export function toValidators( } const max = getConstraint("max", config, runtime); - if (max === undefined) { + if (max !== undefined) { validators.push(Validators.max(max)); }