Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,6 @@ This section provides clear and concise instructions for installing RocketAdmin

## Usage

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>"`
2. You can sign in using these credentials. We recommend to change email and password after first login
1. After installation, open RocketAdmin in your browser at `http://localhost:8080`. You will be redirected to the setup page.
2. On the setup page, create your admin account by entering your email and password.
3. After completing the setup, sign in with your new credentials.
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
<div class="foreign-key">
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{normalizedLabel}}</mat-label>
<mat-spinner *ngIf="fetching" class="loader" diameter="20"></mat-spinner>
@if (fetching()) {
<mat-spinner class="loader" diameter="20"></mat-spinner>
}
<input type="text" matInput
[required]="required" [disabled]="disabled" [readonly]="readonly"
[(ngModel)]="currentDisplayedString"
[matAutocomplete]="auto"
(ngModelChange)="autocmpleteUpdate.next($event)">
(ngModelChange)="onSearchInput()">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="updateRelatedLink($event)">
<mat-option *ngFor="let suggestion of suggestions"
[ngClass]="{'disabled': suggestion.displayString === 'No matches'}"
[value]="suggestion.displayString">
{{suggestion.displayString}}
</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.
[value]="suggestion.displayString">
{{suggestion.displayString}}
</mat-option>
}
</mat-autocomplete>
<mat-hint>Improve search performance by configuring <em>Foreign key search fields</em>&nbsp;
<a routerLink="/dashboard/{{connectionID}}/{{relations.referenced_table_name}}/settings" target="_blank" class="hint-link">here</a>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,22 @@ describe('ForeignKeyFilterComponent', () => {
expect(component).toBeTruthy();
});

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

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

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

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

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

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: 'Taylor (Alex | new-user-5@email.com)',
primaryKeys: { id: 33 },
Expand All @@ -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();

Comment on lines 155 to 198

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.
expect(component.identityColumn).toBeUndefined;
expect(component.currentDisplayedString).toEqual('Alex | Taylor | new-user-5@email.com');
expect(component.currentFieldValue).toEqual(33);

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: 'Alex | Taylor | new-user-5@email.com',
primaryKeys: { id: 33 },
Expand All @@ -217,7 +219,7 @@ describe('ForeignKeyFilterComponent', () => {
]);
});

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

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

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

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

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: '33 | Alex | Taylor | new-user-5@email.com | 24',
primaryKeys: { id: 33 },
Expand All @@ -256,10 +259,10 @@ describe('ForeignKeyFilterComponent', () => {
]);
});

it('should set current value if necessary row is in suggestions list', () => {
it('should set current value if necessary row is in suggestions list', async () => {
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));
fixture.detectChanges();
component.suggestions = [
component.suggestions.set([
{
displayString: 'Alex | Taylor | new-user-5@email.com',
primaryKeys: { id: 33 },
Expand All @@ -275,17 +278,18 @@ describe('ForeignKeyFilterComponent', () => {
primaryKeys: { id: 35 },
fieldValue: 35,
},
];
]);
component.currentDisplayedString = 'Alex | Johnson | new-user-4@email.com';

component.fetchSuggestions();
await component.fetchSuggestions();

expect(component.currentFieldValue).toEqual(34);
});

it('should fetch suggestions list if user types search query and identity column is set', () => {
it('should fetch suggestions list if user types search query and identity column is set', async () => {
vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork));
fixture.detectChanges();

const searchSuggestionsNetwork = {
rows: [
{
Expand All @@ -311,7 +315,7 @@ describe('ForeignKeyFilterComponent', () => {

component.relations = fakeRelations;

component.suggestions = [
component.suggestions.set([
{
displayString: 'Alex | Taylor | new-user-5@email.com',
fieldValue: 33,
Expand All @@ -324,12 +328,12 @@ describe('ForeignKeyFilterComponent', () => {
displayString: 'Alex | Smith | some-new@email.com',
fieldValue: 35,
},
];
]);

component.currentDisplayedString = 'John';
component.fetchSuggestions();
await component.fetchSuggestions();

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: 'Taylor (John | new-user-0@email.com)',
primaryKeys: { id: 23 },
Expand All @@ -343,14 +347,14 @@ describe('ForeignKeyFilterComponent', () => {
]);
});

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

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

component.suggestions = [
component.suggestions.set([
{
displayString: 'Alex | Taylor | new-user-5@email.com',
primaryKeys: { id: 33 },
Expand All @@ -366,19 +370,19 @@ describe('ForeignKeyFilterComponent', () => {
primaryKeys: { id: 35 },
fieldValue: 35,
},
];
]);

component.currentDisplayedString = 'skjfhskjdf';
component.fetchSuggestions();
await component.fetchSuggestions();

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: 'No field starts with "skjfhskjdf" in foreign entity.',
},
]);
});

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

component.suggestions = [
component.suggestions.set([
{
displayString: 'Alex | Taylor | new-user-5@email.com',
fieldValue: 33,
Expand All @@ -416,10 +420,10 @@ describe('ForeignKeyFilterComponent', () => {
displayString: 'Alex | Smith | some-new@email.com',
fieldValue: 35,
},
];
]);

component.currentDisplayedString = 'Alex';
component.fetchSuggestions();
await component.fetchSuggestions();

fixture.detectChanges();

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

expect(component.suggestions).toEqual([
expect(component.suggestions()).toEqual([
{
displayString: 'John | Taylor | new-user-0@email.com',
primaryKeys: { id: 23 },
Expand Down
Loading
Loading