Skip to content

Commit c0882e1

Browse files
Merge pull request #2255 from DonOmalVindula/fix/ou-picker-child-ous
Show OU picker when single root OU has child OUs
2 parents d9f8378 + 32d2127 commit c0882e1

8 files changed

Lines changed: 117 additions & 88 deletions

File tree

frontend/apps/thunder-console/src/features/applications/pages/ApplicationCreatePage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import generateFlowGraph from '../../flows/utils/generateFlowGraph';
3939
import useIdentityProviders from '../../integrations/api/useIdentityProviders';
4040
import {AuthenticatorTypes} from '../../integrations/models/authenticators';
4141
import {IdentityProviderTypes} from '../../integrations/models/identity-provider';
42-
import useGetOrganizationUnits from '../../organization-units/api/useGetOrganizationUnits';
42+
import useHasMultipleOUs from '../../organization-units/api/useHasMultipleOUs';
4343
import useGetUserTypes from '../../user-types/api/useGetUserTypes';
4444
import useCreateApplication from '../api/useCreateApplication';
4545
import ConfigureSignInOptions from '../components/create-application/configure-signin-options/ConfigureSignInOptions';
@@ -115,9 +115,7 @@ export default function ApplicationCreatePage(): JSX.Element {
115115
const logger = useLogger('ApplicationCreatePage');
116116
const createApplication = useCreateApplication();
117117
const {data: userTypesData} = useGetUserTypes();
118-
const {data: ouData, isLoading: isOuLoading} = useGetOrganizationUnits({limit: 2, offset: 0});
119-
const ouList = ouData?.organizationUnits ?? [];
120-
const hasMultipleOUs = (ouData?.totalResults ?? 0) > 1;
118+
const {hasMultipleOUs, isLoading: isOuLoading, ouList} = useHasMultipleOUs();
121119

122120
const [selectedUserTypes, setSelectedUserTypes] = useState<string[]>([]);
123121
const [createdApplication, setCreatedApplication] = useState<Application | null>(null);

frontend/apps/thunder-console/src/features/groups/pages/CreateGroupPage.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {useState, useCallback, useMemo} from 'react';
3333
import type {JSX} from 'react';
3434
import {useTranslation} from 'react-i18next';
3535
import {useNavigate} from 'react-router';
36-
import useGetOrganizationUnits from '../../organization-units/api/useGetOrganizationUnits';
36+
import useHasMultipleOUs from '../../organization-units/api/useHasMultipleOUs';
3737
import useCreateGroup from '../api/useCreateGroup';
3838
import ConfigureName from '../components/create-group/ConfigureName';
3939
import ConfigureOrganizationUnit from '../components/create-group/ConfigureOrganizationUnit';
@@ -49,10 +49,7 @@ export default function CreateGroupPage(): JSX.Element {
4949

5050
const {currentStep, setCurrentStep, name, setName, ouId, setOuId, error, setError} = useGroupCreate();
5151

52-
// Fetch OUs to determine if we need the OU step
53-
const {data: ouData, isLoading: isOuLoading} = useGetOrganizationUnits({limit: 2, offset: 0});
54-
const ouList = ouData?.organizationUnits ?? [];
55-
const hasMultipleOUs = (ouData?.totalResults ?? 0) > 1;
52+
const {hasMultipleOUs, isLoading: isOuLoading, ouList} = useHasMultipleOUs();
5653

5754
const [validationError, setValidationError] = useState<string | null>(null);
5855
const [snackbarOpen, setSnackbarOpen] = useState(false);

frontend/apps/thunder-console/src/features/groups/pages/__tests__/CreateGroupPage.test.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ vi.mock('../../../organization-units/components/OrganizationUnitTreePicker', ()
5353
),
5454
}));
5555

56-
const mockUseGetOrganizationUnits = vi.fn();
57-
vi.mock('../../../organization-units/api/useGetOrganizationUnits', () => ({
58-
default: (...args: unknown[]): unknown => mockUseGetOrganizationUnits(...args),
56+
const mockUseHasMultipleOUs = vi.fn();
57+
vi.mock('../../../organization-units/api/useHasMultipleOUs', () => ({
58+
default: (): unknown => mockUseHasMultipleOUs(),
5959
}));
6060

6161
function renderPage() {
@@ -79,12 +79,10 @@ describe('CreateGroupPage', () => {
7979

8080
describe('with single OU', () => {
8181
beforeEach(() => {
82-
mockUseGetOrganizationUnits.mockReturnValue({
83-
data: {
84-
totalResults: 1,
85-
organizationUnits: [{id: 'ou-single', name: 'Default OU'}],
86-
},
82+
mockUseHasMultipleOUs.mockReturnValue({
83+
hasMultipleOUs: false,
8784
isLoading: false,
85+
ouList: [{id: 'ou-single', name: 'Default OU'}],
8886
});
8987
});
9088

@@ -159,15 +157,13 @@ describe('CreateGroupPage', () => {
159157

160158
describe('with multiple OUs', () => {
161159
beforeEach(() => {
162-
mockUseGetOrganizationUnits.mockReturnValue({
163-
data: {
164-
totalResults: 3,
165-
organizationUnits: [
166-
{id: 'ou-1', name: 'OU 1'},
167-
{id: 'ou-2', name: 'OU 2'},
168-
],
169-
},
160+
mockUseHasMultipleOUs.mockReturnValue({
161+
hasMultipleOUs: true,
170162
isLoading: false,
163+
ouList: [
164+
{id: 'ou-1', name: 'OU 1'},
165+
{id: 'ou-2', name: 'OU 2'},
166+
],
171167
});
172168
});
173169

@@ -316,9 +312,10 @@ describe('CreateGroupPage', () => {
316312
});
317313

318314
it('should handle submission error gracefully', async () => {
319-
mockUseGetOrganizationUnits.mockReturnValue({
320-
data: {totalResults: 1, organizationUnits: [{id: 'ou-single', name: 'Default OU'}]},
315+
mockUseHasMultipleOUs.mockReturnValue({
316+
hasMultipleOUs: false,
321317
isLoading: false,
318+
ouList: [{id: 'ou-single', name: 'Default OU'}],
322319
});
323320
mockMutateAsync.mockRejectedValue(new Error('Create failed'));
324321

@@ -343,9 +340,10 @@ describe('CreateGroupPage', () => {
343340
});
344341

345342
it('should disable continue button while OUs are loading', () => {
346-
mockUseGetOrganizationUnits.mockReturnValue({
347-
data: null,
343+
mockUseHasMultipleOUs.mockReturnValue({
344+
hasMultipleOUs: false,
348345
isLoading: true,
346+
ouList: [],
349347
});
350348

351349
renderPage();
@@ -355,9 +353,10 @@ describe('CreateGroupPage', () => {
355353
});
356354

357355
it('should navigate back when close button is clicked', async () => {
358-
mockUseGetOrganizationUnits.mockReturnValue({
359-
data: {totalResults: 1, organizationUnits: [{id: 'ou-1', name: 'OU 1'}]},
356+
mockUseHasMultipleOUs.mockReturnValue({
357+
hasMultipleOUs: false,
360358
isLoading: false,
359+
ouList: [{id: 'ou-1', name: 'OU 1'}],
361360
});
362361

363362
const user = userEvent.setup();
@@ -372,9 +371,10 @@ describe('CreateGroupPage', () => {
372371
});
373372

374373
it('should handle navigate rejection gracefully', async () => {
375-
mockUseGetOrganizationUnits.mockReturnValue({
376-
data: {totalResults: 1, organizationUnits: [{id: 'ou-1', name: 'OU 1'}]},
374+
mockUseHasMultipleOUs.mockReturnValue({
375+
hasMultipleOUs: false,
377376
isLoading: false,
377+
ouList: [{id: 'ou-1', name: 'OU 1'}],
378378
});
379379
mockNavigate.mockRejectedValue(new Error('Nav failed'));
380380

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import useGetChildOrganizationUnits from './useGetChildOrganizationUnits';
20+
import useGetOrganizationUnits from './useGetOrganizationUnits';
21+
import type {OrganizationUnit} from '../models/organization-unit';
22+
23+
interface UseHasMultipleOUsResult {
24+
hasMultipleOUs: boolean;
25+
isLoading: boolean;
26+
ouList: OrganizationUnit[];
27+
}
28+
29+
export default function useHasMultipleOUs(): UseHasMultipleOUsResult {
30+
const {data: ouData, isLoading: isOuLoading} = useGetOrganizationUnits({limit: 2, offset: 0});
31+
const ouList = ouData?.organizationUnits ?? [];
32+
const rootCount = ouData?.totalResults ?? 0;
33+
const singleRootId = rootCount === 1 ? ouList[0]?.id : undefined;
34+
35+
const {data: childData, isLoading: isChildLoading} = useGetChildOrganizationUnits(singleRootId, {
36+
limit: 1,
37+
offset: 0,
38+
});
39+
40+
const hasMultipleRoots = rootCount > 1;
41+
const singleRootHasChildren = rootCount === 1 && (childData?.totalResults ?? 0) > 0;
42+
43+
return {
44+
hasMultipleOUs: hasMultipleRoots || singleRootHasChildren,
45+
isLoading: isOuLoading || (rootCount === 1 && isChildLoading),
46+
ouList,
47+
};
48+
}

frontend/apps/thunder-console/src/features/roles/pages/CreateRolePage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {useState, useCallback, useMemo} from 'react';
3333
import type {JSX} from 'react';
3434
import {useTranslation} from 'react-i18next';
3535
import {useNavigate} from 'react-router';
36-
import useGetOrganizationUnits from '../../organization-units/api/useGetOrganizationUnits';
36+
import useHasMultipleOUs from '../../organization-units/api/useHasMultipleOUs';
3737
import useCreateRole from '../api/useCreateRole';
3838
import ConfigureBasicInfo from '../components/create-role/ConfigureBasicInfo';
3939
import ConfigureOrganizationUnit from '../components/create-role/ConfigureOrganizationUnit';
@@ -49,9 +49,7 @@ export default function CreateRolePage(): JSX.Element {
4949

5050
const {currentStep, setCurrentStep, name, setName, ouId, setOuId, error, setError} = useRoleCreate();
5151

52-
const {data: ouData, isLoading: isOuLoading} = useGetOrganizationUnits({limit: 2, offset: 0});
53-
const ouList = ouData?.organizationUnits ?? [];
54-
const hasMultipleOUs = (ouData?.totalResults ?? 0) > 1;
52+
const {hasMultipleOUs, isLoading: isOuLoading, ouList} = useHasMultipleOUs();
5553

5654
const [validationError, setValidationError] = useState<string | null>(null);
5755
const [snackbarOpen, setSnackbarOpen] = useState(false);

frontend/apps/thunder-console/src/features/roles/pages/__tests__/CreateRolePage.test.tsx

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import CreateRolePage from '../CreateRolePage';
2525
// Mock dependencies
2626
vi.mock('../../api/useCreateRole');
2727
vi.mock('../../contexts/RoleCreate/useRoleCreate');
28-
vi.mock('../../../organization-units/api/useGetOrganizationUnits');
28+
vi.mock('../../../organization-units/api/useHasMultipleOUs');
2929

3030
vi.mock('../../components/create-role/ConfigureBasicInfo', () => ({
3131
default: () => <div data-testid="configure-basic-info">Configure Basic Info</div>,
@@ -54,7 +54,7 @@ vi.mock('@thunder/logger/react', () => ({
5454

5555
const {default: useCreateRole} = await import('../../api/useCreateRole');
5656
const {default: useRoleCreate} = await import('../../contexts/RoleCreate/useRoleCreate');
57-
const {default: useGetOrganizationUnits} = await import('../../../organization-units/api/useGetOrganizationUnits');
57+
const {default: useHasMultipleOUs} = await import('../../../organization-units/api/useHasMultipleOUs');
5858
const {useNavigate} = await import('react-router');
5959

6060
describe('CreateRolePage', () => {
@@ -104,14 +104,11 @@ describe('CreateRolePage', () => {
104104
variables: undefined,
105105
} as unknown as ReturnType<typeof useCreateRole>);
106106

107-
vi.mocked(useGetOrganizationUnits).mockReturnValue({
108-
data: {
109-
totalResults: 1,
110-
organizationUnits: [{id: 'ou-1', name: 'Default OU'}],
111-
},
107+
vi.mocked(useHasMultipleOUs).mockReturnValue({
108+
hasMultipleOUs: false,
112109
isLoading: false,
113-
error: null,
114-
} as unknown as ReturnType<typeof useGetOrganizationUnits>);
110+
ouList: [{id: 'ou-1', handle: 'default-ou', name: 'Default OU'}],
111+
});
115112
});
116113

117114
afterEach(() => {
@@ -161,17 +158,14 @@ describe('CreateRolePage', () => {
161158
reset: vi.fn(),
162159
} as unknown as ReturnType<typeof useRoleCreate>);
163160

164-
vi.mocked(useGetOrganizationUnits).mockReturnValue({
165-
data: {
166-
totalResults: 2,
167-
organizationUnits: [
168-
{id: 'ou-1', name: 'OU 1'},
169-
{id: 'ou-2', name: 'OU 2'},
170-
],
171-
},
161+
vi.mocked(useHasMultipleOUs).mockReturnValue({
162+
hasMultipleOUs: true,
172163
isLoading: false,
173-
error: null,
174-
} as unknown as ReturnType<typeof useGetOrganizationUnits>);
164+
ouList: [
165+
{id: 'ou-1', handle: 'ou-1', name: 'OU 1'},
166+
{id: 'ou-2', handle: 'ou-2', name: 'OU 2'},
167+
],
168+
});
175169

176170
render(<CreateRolePage />);
177171

@@ -191,17 +185,14 @@ describe('CreateRolePage', () => {
191185
reset: vi.fn(),
192186
} as unknown as ReturnType<typeof useRoleCreate>);
193187

194-
vi.mocked(useGetOrganizationUnits).mockReturnValue({
195-
data: {
196-
totalResults: 2,
197-
organizationUnits: [
198-
{id: 'ou-1', name: 'OU 1'},
199-
{id: 'ou-2', name: 'OU 2'},
200-
],
201-
},
188+
vi.mocked(useHasMultipleOUs).mockReturnValue({
189+
hasMultipleOUs: true,
202190
isLoading: false,
203-
error: null,
204-
} as unknown as ReturnType<typeof useGetOrganizationUnits>);
191+
ouList: [
192+
{id: 'ou-1', handle: 'ou-1', name: 'OU 1'},
193+
{id: 'ou-2', handle: 'ou-2', name: 'OU 2'},
194+
],
195+
});
205196

206197
render(<CreateRolePage />);
207198

frontend/apps/thunder-console/src/features/user-types/components/create-user-type/ConfigureGeneral.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {Typography, Stack, FormControl, FormLabel, Checkbox, FormControlLabel} f
2020
import type {JSX} from 'react';
2121
import {useEffect} from 'react';
2222
import {useTranslation} from 'react-i18next';
23-
import useGetOrganizationUnits from '../../../organization-units/api/useGetOrganizationUnits';
23+
import useHasMultipleOUs from '../../../organization-units/api/useHasMultipleOUs';
2424
import OrganizationUnitTreePicker from '../../../organization-units/components/OrganizationUnitTreePicker';
2525

2626
/**
@@ -49,14 +49,14 @@ export default function ConfigureGeneral({
4949
onReadyChange = undefined,
5050
}: ConfigureGeneralProps): JSX.Element {
5151
const {t} = useTranslation();
52-
const {data} = useGetOrganizationUnits();
52+
const {hasMultipleOUs, ouList} = useHasMultipleOUs();
5353

54-
// Auto-select first organization unit
54+
// Auto-select first organization unit when none is selected
5555
useEffect(() => {
56-
if (!ouId && data?.organizationUnits && data.organizationUnits.length > 0) {
57-
onOuIdChange(data.organizationUnits[0].id);
56+
if (!ouId && ouList.length > 0) {
57+
onOuIdChange(ouList[0].id);
5858
}
59-
}, [data, ouId, onOuIdChange]);
59+
}, [ouList, ouId, onOuIdChange]);
6060

6161
// Broadcast readiness
6262
useEffect((): void => {
@@ -76,10 +76,12 @@ export default function ConfigureGeneral({
7676
</Typography>
7777
</Stack>
7878

79-
<FormControl fullWidth required>
80-
<FormLabel>{t('userTypes:organizationUnit')}</FormLabel>
81-
<OrganizationUnitTreePicker id="user-type-ou-picker" value={ouId} onChange={onOuIdChange} />
82-
</FormControl>
79+
{hasMultipleOUs && (
80+
<FormControl fullWidth required>
81+
<FormLabel>{t('userTypes:organizationUnit')}</FormLabel>
82+
<OrganizationUnitTreePicker id="user-type-ou-picker" value={ouId} onChange={onOuIdChange} />
83+
</FormControl>
84+
)}
8385

8486
<FormControlLabel
8587
control={

frontend/apps/thunder-console/src/features/user-types/pages/__tests__/CreateUserTypePage.test.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,15 @@ vi.mock('../../api/useCreateUserType', () => ({
5454
default: () => mockUseCreateUserType(),
5555
}));
5656

57-
// Mock useGetOrganizationUnits (used by ConfigureGeneral for auto-selecting the first OU)
58-
vi.mock('../../../organization-units/api/useGetOrganizationUnits', () => ({
57+
// Mock useHasMultipleOUs (used by ConfigureGeneral to decide whether to show the OU picker)
58+
vi.mock('../../../organization-units/api/useHasMultipleOUs', () => ({
5959
default: () => ({
60-
data: {
61-
totalResults: 2,
62-
startIndex: 1,
63-
count: 2,
64-
organizationUnits: [
65-
{id: 'root-ou', name: 'Root Organization', handle: 'root', description: null, parent: null},
66-
{id: 'child-ou', name: 'Child Organization', handle: 'child', description: null, parent: 'root-ou'},
67-
],
68-
},
60+
hasMultipleOUs: true,
6961
isLoading: false,
70-
error: null,
62+
ouList: [
63+
{id: 'root-ou', name: 'Root Organization', handle: 'root', description: null, parent: null},
64+
{id: 'child-ou', name: 'Child Organization', handle: 'child', description: null, parent: 'root-ou'},
65+
],
7166
}),
7267
}));
7368

0 commit comments

Comments
 (0)