Skip to content

Commit 461f78d

Browse files
guguclaude
andauthored
refactor: migrate field widget components from RxJS to Angular signals (#1581)
* refactor: migrate field widget components from RxJS to Angular signals 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> * docs: update setup instructions to reflect new setup page 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> * Refactor country and language components: replace effect with computed 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> * remove effect usage --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aec08c8 commit 461f78d

File tree

14 files changed

+1390
-1260
lines changed

14 files changed

+1390
-1260
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,6 @@ This section provides clear and concise instructions for installing RocketAdmin
100100

101101
## Usage
102102

103-
1. After installation rocketadmin will create a user with email admin@email.local and autogenerated password. The message will be `Admin user created with email: "admin@email.local" and password: "<password>"`
104-
2. You can sign in using these credentials. We recommend to change email and password after first login
103+
1. After installation, open RocketAdmin in your browser at `http://localhost:8080`. You will be redirected to the setup page.
104+
2. On the setup page, create your admin account by entering your email and password.
105+
3. After completing the setup, sign in with your new credentials.

frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
<div class="foreign-key">
55
<mat-form-field class="full-width" appearance="outline">
66
<mat-label>{{normalizedLabel}}</mat-label>
7-
<mat-spinner *ngIf="fetching" class="loader" diameter="20"></mat-spinner>
7+
@if (fetching()) {
8+
<mat-spinner class="loader" diameter="20"></mat-spinner>
9+
}
810
<input type="text" matInput
911
[required]="required" [disabled]="disabled" [readonly]="readonly"
1012
[(ngModel)]="currentDisplayedString"
1113
[matAutocomplete]="auto"
12-
(ngModelChange)="autocmpleteUpdate.next($event)">
14+
(ngModelChange)="onSearchInput()">
1315
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="updateRelatedLink($event)">
14-
<mat-option *ngFor="let suggestion of suggestions"
15-
[ngClass]="{'disabled': suggestion.displayString === 'No matches'}"
16-
[value]="suggestion.displayString">
17-
{{suggestion.displayString}}
18-
</mat-option>
16+
@for (suggestion of suggestions(); track suggestion.fieldValue) {
17+
<mat-option
18+
[ngClass]="{'disabled': suggestion.displayString === 'No matches'}"
19+
[value]="suggestion.displayString">
20+
{{suggestion.displayString}}
21+
</mat-option>
22+
}
1923
</mat-autocomplete>
2024
<mat-hint>Improve search performance by configuring <em>Foreign key search fields</em>&nbsp;
2125
<a routerLink="/dashboard/{{connectionID}}/{{relations.referenced_table_name}}/settings" target="_blank" class="hint-link">here</a>.

frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -152,21 +152,22 @@ describe('ForeignKeyFilterComponent', () => {
152152
expect(component).toBeTruthy();
153153
});
154154

155-
it('should fill initial dropdown values when identity_column is set', () => {
155+
it('should fill initial dropdown values when identity_column is set', async () => {
156156
const usersTableNetworkWithIdentityColumn = { ...usersTableNetwork, identity_column: 'lastname' };
157157

158158
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetworkWithIdentityColumn));
159159

160160
component.connectionID = '12345678';
161161
component.value = '33'; // Must be truthy to trigger currentDisplayedString setting
162162

163-
fixture.detectChanges(); // This triggers ngOnInit
163+
await component.ngOnInit();
164+
fixture.detectChanges();
164165

165166
expect(component.identityColumn).toEqual('lastname');
166167
expect(component.currentDisplayedString).toEqual('Taylor (Alex | new-user-5@email.com)');
167168
expect(component.currentFieldValue).toEqual(33);
168169

169-
expect(component.suggestions).toEqual([
170+
expect(component.suggestions()).toEqual([
170171
{
171172
displayString: 'Taylor (Alex | new-user-5@email.com)',
172173
primaryKeys: { id: 33 },
@@ -185,20 +186,21 @@ describe('ForeignKeyFilterComponent', () => {
185186
]);
186187
});
187188

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

191192
component.connectionID = '12345678';
192193

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

195-
fixture.detectChanges(); // This triggers ngOnInit
196+
await component.ngOnInit();
197+
fixture.detectChanges();
196198

197199
expect(component.identityColumn).toBeUndefined;
198200
expect(component.currentDisplayedString).toEqual('Alex | Taylor | new-user-5@email.com');
199201
expect(component.currentFieldValue).toEqual(33);
200202

201-
expect(component.suggestions).toEqual([
203+
expect(component.suggestions()).toEqual([
202204
{
203205
displayString: 'Alex | Taylor | new-user-5@email.com',
204206
primaryKeys: { id: 33 },
@@ -217,7 +219,7 @@ describe('ForeignKeyFilterComponent', () => {
217219
]);
218220
});
219221

220-
it('should fill initial dropdown values when autocomplete_columns is not set', () => {
222+
it('should fill initial dropdown values when autocomplete_columns is not set', async () => {
221223
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));
222224

223225
component.connectionID = '12345678';
@@ -231,13 +233,14 @@ describe('ForeignKeyFilterComponent', () => {
231233
};
232234
component.value = '33'; // Must be truthy to trigger currentDisplayedString setting
233235

234-
fixture.detectChanges(); // This triggers ngOnInit
236+
await component.ngOnInit();
237+
fixture.detectChanges();
235238

236239
expect(component.identityColumn).toBeUndefined;
237240
expect(component.currentDisplayedString).toEqual('33 | Alex | Taylor | new-user-5@email.com | 24');
238241
expect(component.currentFieldValue).toEqual(33);
239242

240-
expect(component.suggestions).toEqual([
243+
expect(component.suggestions()).toEqual([
241244
{
242245
displayString: '33 | Alex | Taylor | new-user-5@email.com | 24',
243246
primaryKeys: { id: 33 },
@@ -256,10 +259,10 @@ describe('ForeignKeyFilterComponent', () => {
256259
]);
257260
});
258261

259-
it('should set current value if necessary row is in suggestions list', () => {
262+
it('should set current value if necessary row is in suggestions list', async () => {
260263
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));
261264
fixture.detectChanges();
262-
component.suggestions = [
265+
component.suggestions.set([
263266
{
264267
displayString: 'Alex | Taylor | new-user-5@email.com',
265268
primaryKeys: { id: 33 },
@@ -275,17 +278,18 @@ describe('ForeignKeyFilterComponent', () => {
275278
primaryKeys: { id: 35 },
276279
fieldValue: 35,
277280
},
278-
];
281+
]);
279282
component.currentDisplayedString = 'Alex | Johnson | new-user-4@email.com';
280283

281-
component.fetchSuggestions();
284+
await component.fetchSuggestions();
282285

283286
expect(component.currentFieldValue).toEqual(34);
284287
});
285288

286-
it('should fetch suggestions list if user types search query and identity column is set', () => {
289+
it('should fetch suggestions list if user types search query and identity column is set', async () => {
287290
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));
288291
fixture.detectChanges();
292+
289293
const searchSuggestionsNetwork = {
290294
rows: [
291295
{
@@ -311,7 +315,7 @@ describe('ForeignKeyFilterComponent', () => {
311315

312316
component.relations = fakeRelations;
313317

314-
component.suggestions = [
318+
component.suggestions.set([
315319
{
316320
displayString: 'Alex | Taylor | new-user-5@email.com',
317321
fieldValue: 33,
@@ -324,12 +328,12 @@ describe('ForeignKeyFilterComponent', () => {
324328
displayString: 'Alex | Smith | some-new@email.com',
325329
fieldValue: 35,
326330
},
327-
];
331+
]);
328332

329333
component.currentDisplayedString = 'John';
330-
component.fetchSuggestions();
334+
await component.fetchSuggestions();
331335

332-
expect(component.suggestions).toEqual([
336+
expect(component.suggestions()).toEqual([
333337
{
334338
displayString: 'Taylor (John | new-user-0@email.com)',
335339
primaryKeys: { id: 23 },
@@ -343,14 +347,14 @@ describe('ForeignKeyFilterComponent', () => {
343347
]);
344348
});
345349

346-
it('should fetch suggestions list if user types search query and show No matches message if the list is empty', () => {
350+
it('should fetch suggestions list if user types search query and show No matches message if the list is empty', async () => {
347351
const searchSuggestionsNetwork = {
348352
rows: [],
349353
};
350354

351355
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(searchSuggestionsNetwork));
352356

353-
component.suggestions = [
357+
component.suggestions.set([
354358
{
355359
displayString: 'Alex | Taylor | new-user-5@email.com',
356360
primaryKeys: { id: 33 },
@@ -366,19 +370,19 @@ describe('ForeignKeyFilterComponent', () => {
366370
primaryKeys: { id: 35 },
367371
fieldValue: 35,
368372
},
369-
];
373+
]);
370374

371375
component.currentDisplayedString = 'skjfhskjdf';
372-
component.fetchSuggestions();
376+
await component.fetchSuggestions();
373377

374-
expect(component.suggestions).toEqual([
378+
expect(component.suggestions()).toEqual([
375379
{
376380
displayString: 'No field starts with "skjfhskjdf" in foreign entity.',
377381
},
378382
]);
379383
});
380384

381-
it('should fetch suggestions list if user types search query and identity column is not set', () => {
385+
it('should fetch suggestions list if user types search query and identity column is not set', async () => {
382386
const searchSuggestionsNetwork = {
383387
rows: [
384388
{
@@ -403,7 +407,7 @@ describe('ForeignKeyFilterComponent', () => {
403407
component.connectionID = '12345678';
404408
component.relations = fakeRelations;
405409

406-
component.suggestions = [
410+
component.suggestions.set([
407411
{
408412
displayString: 'Alex | Taylor | new-user-5@email.com',
409413
fieldValue: 33,
@@ -416,10 +420,10 @@ describe('ForeignKeyFilterComponent', () => {
416420
displayString: 'Alex | Smith | some-new@email.com',
417421
fieldValue: 35,
418422
},
419-
];
423+
]);
420424

421425
component.currentDisplayedString = 'Alex';
422-
component.fetchSuggestions();
426+
await component.fetchSuggestions();
423427

424428
fixture.detectChanges();
425429

@@ -433,7 +437,7 @@ describe('ForeignKeyFilterComponent', () => {
433437
referencedColumn: component.relations.referenced_column_name,
434438
});
435439

436-
expect(component.suggestions).toEqual([
440+
expect(component.suggestions()).toEqual([
437441
{
438442
displayString: 'John | Taylor | new-user-0@email.com',
439443
primaryKeys: { id: 23 },

0 commit comments

Comments
 (0)