Skip to content

Commit 8dfbe15

Browse files
committed
feat: add organization-management component & UserProfileConfigServiceImpl
1 parent c29dc1c commit 8dfbe15

12 files changed

+479
-13
lines changed

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"@angular-builders/jest": "^20.0.0",
1717
"@angular-devkit/build-angular": "^20.0.0",
1818
"@angular-eslint/builder": "^20.2.0",
19-
"@angular/cli": "^20.2.0",
2019
"@angular/build": "^20.2.1",
20+
"@angular/cli": "^20.2.0",
2121
"@angular/compiler-cli": "^20.2.1",
2222
"@angular/localize": "^20.2.1",
2323
"@briebug/jest-schematic": "^6.0.0",
@@ -30,11 +30,11 @@
3030
"jest-junit": "16.0.0",
3131
"jest-mock-extended": "3.0.7",
3232
"jmespath": "0.16.0",
33+
"mkdirp": "^3.0.1",
3334
"ng-packagr": "^20.2.0",
34-
"ts-jest": "29.3.2",
35-
"typescript": "~5.8.0",
35+
"nodemon": "3.1.10",
3636
"rimraf": "6.0.1",
37-
"mkdirp": "^3.0.1",
38-
"nodemon": "3.1.10"
37+
"ts-jest": "29.3.2",
38+
"typescript": "~5.8.0"
3939
}
4040
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<div class="organization-management">
2+
<div>
3+
{{ texts.explanation }}
4+
</div>
5+
6+
<div>
7+
<ui5-label for="select-switch" show-colon>{{
8+
texts.switchOrganization.label
9+
}}</ui5-label
10+
><br />
11+
<div class="organization-management-input">
12+
<ui5-select
13+
id="select-switch"
14+
[value]="organizationToSwitch"
15+
(input)="setOrganizationToSwitch($event)"
16+
(change)="setOrganizationToSwitch($event)"
17+
>
18+
@for (org of organizations(); track org) {
19+
<ui5-option [value]="org" [selected]="org === organizationToSwitch">{{
20+
org
21+
}}</ui5-option>
22+
}
23+
</ui5-select>
24+
<ui5-button design="Emphasized" (ui5Click)="switchOrganization()">{{
25+
texts.switchOrganization.button
26+
}}</ui5-button>
27+
</div>
28+
</div>
29+
30+
<div>
31+
<ui5-label for="input-onboard" show-colon>{{
32+
texts.onboardOrganization.label
33+
}}</ui5-label
34+
><br />
35+
<div class="organization-management-input">
36+
<ui5-input
37+
id="input-onboard"
38+
placeholder="{{ texts.onboardOrganization.placeholder }}"
39+
[(ngModel)]="newOrganization"
40+
></ui5-input>
41+
<ui5-button design="Emphasized" (ui5Click)="onboardOrganization()">{{
42+
texts.onboardOrganization.button
43+
}}</ui5-button>
44+
</div>
45+
</div>
46+
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.organization-management {
2+
margin: 1.5rem;
3+
display: flex;
4+
flex-direction: column;
5+
gap: 1rem;
6+
}
7+
8+
.organization-management-input {
9+
display: flex;
10+
align-items: center;
11+
gap: 1rem;
12+
}
13+
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {
2+
CUSTOM_ELEMENTS_SCHEMA,
3+
NO_ERRORS_SCHEMA,
4+
signal
5+
} from '@angular/core';
6+
import { ComponentFixture, TestBed } from '@angular/core/testing';
7+
import { FormsModule } from '@angular/forms';
8+
import { MutationResult } from '@apollo/client';
9+
import {
10+
ClientEnvironment, EnvConfigService,
11+
I18nService,
12+
LuigiCoreService, LuigiGlobalContext, NodeContext, ResourceService
13+
} from '@openmfp/portal-ui-lib';
14+
import { of, throwError } from 'rxjs';
15+
import { OrganizationManagementComponent } from './organization-management.component';
16+
17+
describe('OrganizationManagementComponent', () => {
18+
let component: OrganizationManagementComponent;
19+
let fixture: ComponentFixture<OrganizationManagementComponent>;
20+
let resourceServiceMock: jest.Mocked<ResourceService>;
21+
let i18nServiceMock: jest.Mocked<I18nService>;
22+
let luigiCoreServiceMock: jest.Mocked<LuigiCoreService>;
23+
let envConfigServiceMock: jest.Mocked<EnvConfigService>;
24+
25+
beforeEach(async () => {
26+
resourceServiceMock = {
27+
readOrganizations: jest.fn(),
28+
create: jest.fn(),
29+
} as any;
30+
31+
i18nServiceMock = {
32+
translationTable: {},
33+
getTranslation: jest.fn(),
34+
} as any;
35+
36+
luigiCoreServiceMock = {
37+
getGlobalContext: jest.fn(),
38+
showAlert: jest.fn(),
39+
} as any;
40+
41+
envConfigServiceMock = {
42+
getEnvConfig: jest.fn(),
43+
} as any;
44+
45+
await TestBed.configureTestingModule({
46+
imports: [OrganizationManagementComponent, FormsModule],
47+
providers: [
48+
{ provide: ResourceService, useValue: resourceServiceMock },
49+
{ provide: I18nService, useValue: i18nServiceMock },
50+
{ provide: LuigiCoreService, useValue: luigiCoreServiceMock },
51+
{ provide: EnvConfigService, useValue: envConfigServiceMock },
52+
],
53+
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
54+
})
55+
.overrideComponent(OrganizationManagementComponent, {
56+
set: { template: '' },
57+
})
58+
.compileComponents();
59+
60+
fixture = TestBed.createComponent(OrganizationManagementComponent);
61+
component = fixture.componentInstance;
62+
});
63+
64+
it('should create', () => {
65+
expect(component).toBeTruthy();
66+
});
67+
68+
it('should react to context input change', () => {
69+
const mockContext = {
70+
translationTable: { hello: 'world' },
71+
} as any as NodeContext;
72+
73+
resourceServiceMock.readOrganizations.mockReturnValue(of({} as any));
74+
75+
const contextSignal = signal<NodeContext | null>(mockContext);
76+
component.context = contextSignal as any;
77+
78+
fixture.detectChanges();
79+
80+
expect(component['i18nService'].translationTable).toEqual(
81+
mockContext.translationTable,
82+
);
83+
});
84+
85+
it('should initialize with empty organizations', () => {
86+
expect(component.organizations()).toEqual([]);
87+
});
88+
89+
it('should read organizations on init', () => {
90+
const mockOrganizations = {
91+
Accounts: [
92+
{ metadata: { name: 'org1' } },
93+
{ metadata: { name: 'org2' } },
94+
],
95+
};
96+
const mockGlobalContext: LuigiGlobalContext = {
97+
portalContext: {},
98+
userId: 'user1',
99+
userEmail: '[email protected]',
100+
token: 'token',
101+
organization: 'org1',
102+
portalBaseUrl: 'https://test.com',
103+
};
104+
luigiCoreServiceMock.getGlobalContext.mockReturnValue(mockGlobalContext);
105+
resourceServiceMock.readOrganizations.mockReturnValue(
106+
of(mockOrganizations as any),
107+
);
108+
109+
component.ngOnInit();
110+
111+
expect(resourceServiceMock.readOrganizations).toHaveBeenCalled();
112+
expect(component.organizations()).toEqual(['org2']);
113+
});
114+
115+
it('should set organization to switch', () => {
116+
const event = { target: { value: 'testOrg' } };
117+
component.setOrganizationToSwitch(event);
118+
expect(component.organizationToSwitch).toBe('testOrg');
119+
});
120+
121+
it('should onboard new organization successfully', () => {
122+
const mockResponse: MutationResult<void> = {
123+
data: undefined,
124+
loading: false,
125+
error: undefined,
126+
called: true,
127+
client: {} as any,
128+
reset: jest.fn(),
129+
};
130+
resourceServiceMock.create.mockReturnValue(of(mockResponse));
131+
component.newOrganization = 'newOrg';
132+
component.organizations.set(['existingOrg']);
133+
134+
component.onboardOrganization();
135+
136+
expect(resourceServiceMock.create).toHaveBeenCalled();
137+
expect(component.organizations()).toEqual(['newOrg', 'existingOrg']);
138+
expect(component.organizationToSwitch).toBe('newOrg');
139+
expect(component.newOrganization).toBe('');
140+
});
141+
142+
it('should handle organization creation error', () => {
143+
resourceServiceMock.create.mockReturnValue(
144+
throwError(() => new Error('Creation failed')),
145+
);
146+
component.newOrganization = 'newOrg';
147+
148+
component.onboardOrganization();
149+
150+
expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({
151+
text: 'Failure! Could not create organization: newOrg.',
152+
type: 'error',
153+
});
154+
});
155+
156+
it('should switch organization', async () => {
157+
const mockEnvConfig: ClientEnvironment = {
158+
idpName: 'test',
159+
organization: 'test',
160+
oauthServerUrl: 'https://test.com',
161+
clientId: 'test',
162+
baseDomain: 'test.com',
163+
isLocal: false,
164+
developmentInstance: false,
165+
authData: {
166+
expires_in: '3600',
167+
access_token: 'test-access-token',
168+
id_token: 'test-id-token',
169+
},
170+
};
171+
envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig);
172+
component.organizationToSwitch = 'newOrg';
173+
Object.defineProperty(window, 'location', {
174+
value: { protocol: 'https:', port: '8080' },
175+
writable: true,
176+
});
177+
178+
await component.switchOrganization();
179+
180+
expect(window.location.href).toBe('https://newOrg.test.com:8080');
181+
});
182+
});

0 commit comments

Comments
 (0)