Skip to content

refactor: migrate field widget components from RxJS to Angular signals#1581

Merged
gugu merged 8 commits into
mainfrom
feat/field-widgets-rxjs-to-signals
Feb 16, 2026
Merged

refactor: migrate field widget components from RxJS to Angular signals#1581
gugu merged 8 commits into
mainfrom
feat/field-widgets-rxjs-to-signals

Conversation

@gugu

@gugu gugu commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Replace RxJS patterns (Subject, Observable, subscribe, pipe operators) with Angular signals (signal, computed, toSignal, effect) in 5 field widget components: language, country, phone, foreign-key edit, and foreign-key filter. Also replace any types with proper interfaces throughout.

Replace RxJS patterns (Subject, Observable, subscribe, pipe operators) with
Angular signals (signal, computed, toSignal, effect) in 5 field widget
components: language, country, phone, foreign-key edit, and foreign-key filter.
Also replace any types with proper interfaces throughout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 9, 2026 08:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors several field widget components to replace RxJS-based streams/subscriptions with Angular signals (signal, computed, toSignal, effect) and tightens types by replacing any with more specific interfaces.

Changes:

  • Migrated autocomplete/filtering logic from Observable + async pipe to signals + computed() in phone/language/country widgets.
  • Refactored foreign-key edit/filter widgets to use signals for state (suggestions, fetching, debounced search term) and firstValueFrom for async fetching.
  • Updated unit tests to accommodate signal-based state and async initialization.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts Replaces filteredCountries$ observable with filteredCountries computed signal derived from FormControl.valueChanges.
frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html Switches to @for + signal invocation (filteredCountries()) and @if blocks.
frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts Adjusts typings/casts for updated FormControl value types and widget structure typing.
frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts Replaces RxJS pipeline with toSignal + computed; adds an effect to keep the flag in sync.
frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html Uses filteredLanguages() instead of `filteredLanguages
frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts Converts country autocomplete and flag selection logic to signals/computed, removing RxJS.
frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html Uses signals (selectedCountryFlag(), showFlag(), filteredCountries()) and @for/@if.
frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts Migrates suggestions/fetching state to signals and introduces debounced search via signals + rxjs-interop.
frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html Uses fetching()/suggestions() signals and @if/@for syntax.
frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts Updates assertions and setup for signal-based state and async component init.
frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.ts Same signal-based migration for the filter variant (debounced search + signal state).
frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html Uses fetching()/suggestions() and @if/@for syntax.
frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts Updates assertions and init flow for signal-based state and async init.
Comments suppressed due to low confidence (1)

frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html:19

  • The template disables options when suggestion.displayString === 'No matches', but the component actually sets the empty-result message to No field starts with "..." in foreign entity.. As a result, the "no results" option won't get the disabled styling/behavior. Align the check with the actual message (or add a dedicated boolean like suggestion.isPlaceholder).
                <mat-option
                    [ngClass]="{'disabled': suggestion.displayString === 'No matches'}"
                    [value]="suggestion.displayString">
                    {{suggestion.displayString}}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +73
effect(() => {
const _term = this._debouncedTerm();
if (this.currentDisplayedString === '') this.onFieldChange.emit(null);
this.fetchSuggestions();
});

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect() in the constructor runs immediately, before Angular sets @Input()s like relations/widgetStructure and before ngOnInit initializes fkRelations/currentDisplayedString. As written, it calls fetchSuggestions() right away, which can dereference this.fkRelations while it's still null/undefined and trigger a failing fetch with an undefined search term. Move this effect setup to ngOnInit after fkRelations is set, or guard the effect (if (!this.fkRelations) return; and ensure currentDisplayedString defaults to '').

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +171
console.log(this.label + 'this.currentFieldValue');
console.log(this.currentFieldValue);
this.onFieldChange.emit(this.currentFieldValue);

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover debug logging in fetchSuggestions() (console.log(...)) will spam the console during normal autocomplete usage and can leak values into logs. Remove these logs or replace with a properly gated logger/debug flag if needed.

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +249
this.currentFieldQueryParams = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
).primaryKeys;

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateRelatedLink assumes find(...) always returns a suggestion and immediately dereferences .primaryKeys. If the selected value isn't found (or if the "no results" placeholder is selected), this will throw at runtime. Add a null-check around the find result and only update currentFieldQueryParams when primaryKeys is present.

Suggested change
this.currentFieldQueryParams = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
).primaryKeys;
const selectedSuggestion = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
);
if (selectedSuggestion && selectedSuggestion.primaryKeys) {
this.currentFieldQueryParams = selectedSuggestion.primaryKeys;
}

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +71
effect(() => {
const _term = this._debouncedTerm();
if (this.currentDisplayedString === '') this.onFieldChange.emit(null);
this.fetchSuggestions();
});

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect() in the constructor runs immediately, before Angular sets @Input()s like relations and before ngOnInit runs. It calls fetchSuggestions() right away, which can dereference this.relations while it's still undefined and trigger a failing fetch with an undefined search term. Move this effect setup to ngOnInit after inputs are available, or guard the effect (if (!this.relations) return; and ensure currentDisplayedString defaults to '').

Copilot uses AI. Check for mistakes.
</mat-option>
@for (suggestion of suggestions(); track suggestion.fieldValue) {
<mat-option
[ngClass]="{'disabled': suggestion.displayString === 'No matches'}"

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template disables options when suggestion.displayString === 'No matches', but the component sets the empty-result message to No field starts with "..." in foreign entity.. This means the "no results" option won't be disabled/styled as intended. Align the check with the actual message or add an explicit flag on the suggestion.

Suggested change
[ngClass]="{'disabled': suggestion.displayString === 'No matches'}"
[ngClass]="{'disabled': suggestion.displayString?.startsWith('No field starts with')}"

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +239
this.currentFieldQueryParams = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
).primaryKeys;

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateRelatedLink assumes find(...) always returns a suggestion and immediately dereferences .primaryKeys. If the selected value isn't found (or if the "no results" placeholder is selected), this will throw at runtime. Add a null-check around the find result and only update currentFieldQueryParams when primaryKeys is present.

Suggested change
this.currentFieldQueryParams = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
).primaryKeys;
const selectedSuggestion = this.suggestions().find(
(suggestion) => suggestion.displayString === e.option.value,
);
if (selectedSuggestion && selectedSuggestion.primaryKeys) {
this.currentFieldQueryParams = selectedSuggestion.primaryKeys;
}

Copilot uses AI. Check for mistakes.
Comment on lines 155 to 198
@@ -185,20 +186,21 @@ describe('ForeignKeyFilterComponent', () => {
]);
});

it('should fill initial dropdown values when identity_column is not set', () => {
it('should fill initial dropdown values when identity_column is not set', async () => {
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));

component.connectionID = '12345678';

component.value = '33'; // Must be truthy to trigger currentDisplayedString setting

fixture.detectChanges(); // This triggers ngOnInit
await component.ngOnInit();
fixture.detectChanges();

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are manually calling ngOnInit() and then calling fixture.detectChanges(). Since detectChanges() also runs lifecycle hooks, ngOnInit can execute twice, causing duplicate fetchTable calls and making tests flaky. Prefer fixture.detectChanges(); await fixture.whenStable(); without manual ngOnInit(), or avoid calling detectChanges() after a manual init.

Copilot uses AI. Check for mistakes.
);
}

displayCountryFn(country: CountryCode): string {

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

displayCountryFn is used as mat-autocomplete's displayWith, and the form control value can be a string while the user is typing (the control is typed as CountryCode | string | null). With the current signature, Angular Material may call displayCountryFn with a string, causing runtime errors when reading country.flag/name/dialCode. Update the function to accept CountryCode | string | null and return the string value unchanged when a string is provided.

Suggested change
displayCountryFn(country: CountryCode): string {
displayCountryFn(country: CountryCode | string | null): string {
if (typeof country === 'string') {
return country;
}

Copilot uses AI. Check for mistakes.

component.ngOnInit();
await component.ngOnInit();
fixture.detectChanges();

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are manually calling ngOnInit() and then calling fixture.detectChanges(). detectChanges() will also run Angular lifecycle hooks, so ngOnInit can end up executing twice, leading to duplicate fetchTable calls and brittle assertions. Prefer triggering init via fixture.detectChanges(); await fixture.whenStable(); (and remove the explicit ngOnInit() call), or avoid calling detectChanges() after a manual ngOnInit() call.

Suggested change
fixture.detectChanges();

Copilot uses AI. Check for mistakes.
gugu and others added 2 commits February 9, 2026 11:14
Replace auto-generated credentials documentation with setup page flow,
as RocketAdmin now redirects to a setup page for initial admin account creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gugu gugu requested a review from lyubov-voloshko February 9, 2026 20:18
gugu and others added 5 commits February 10, 2026 11:52
…d for flag derivation

The selectedFlag was a writable signal updated via effect(), but it's purely derived
from the form control value — computed() is the correct primitive here.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gugu gugu enabled auto-merge (squash) February 16, 2026 10:15
@gugu gugu merged commit 461f78d into main Feb 16, 2026
17 of 19 checks passed
@gugu gugu deleted the feat/field-widgets-rxjs-to-signals branch February 16, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants