Skip to content

Commit 72be432

Browse files
Merge branch 'main' into chore/navigation-refactoring
2 parents 18495fb + 1a553c4 commit 72be432

29 files changed

+460
-164
lines changed

codecov.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coverage:
2+
status:
3+
project: off

development/azure-devops-mock/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "1.0.0",
55
"author": "The Altinn Studio Team",
66
"dependencies": {
7-
"axios": "1.7.9",
7+
"axios": "1.8.2",
88
"cors": "2.8.5",
99
"express": "4.21.2",
1010
"morgan": "1.10.0",

frontend/app-development/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@studio/hooks": "workspace:^",
1515
"@studio/icons": "workspace:^",
1616
"@studio/pure-functions": "workspace:^",
17-
"axios": "1.7.9",
17+
"axios": "1.8.2",
1818
"classnames": "2.5.1",
1919
"i18next": "23.16.8",
2020
"react": "18.3.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { HeaderContextProvider, useHeaderContext } from './HeaderContext';
4+
import { renderWithProviders } from '../../testing/mocks';
5+
6+
describe('HeaderContext', () => {
7+
it('should render children', () => {
8+
renderWithProviders(
9+
<HeaderContextProvider>
10+
<button>My button</button>
11+
</HeaderContextProvider>,
12+
);
13+
14+
expect(screen.getByRole('button', { name: 'My button' })).toBeInTheDocument();
15+
});
16+
17+
it('should provide a useHeaderContext hook', () => {
18+
const TestComponent = () => {
19+
const {} = useHeaderContext();
20+
return <div data-testid='context'></div>;
21+
};
22+
23+
renderWithProviders(
24+
<HeaderContextProvider>
25+
<TestComponent />
26+
</HeaderContextProvider>,
27+
);
28+
29+
expect(screen.getByTestId('context')).toHaveTextContent('');
30+
});
31+
32+
it('should throw an error when useHeaderContext is used outside of a HeaderContextProvider', () => {
33+
// Mock console error to check if it has been called
34+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
35+
const TestComponent = () => {
36+
useHeaderContext();
37+
return <div data-testid='context'>Test</div>;
38+
};
39+
40+
expect(() => render(<TestComponent />)).toThrow(
41+
'useHeaderContext must be used within a HeaderContextProvider',
42+
);
43+
expect(consoleError).toHaveBeenCalled();
44+
});
45+
});
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,114 @@
1-
import { createContext } from 'react';
1+
import React, { createContext, useContext } from 'react';
2+
import type { ReactElement, ReactNode } from 'react';
3+
import { useNavigate } from 'react-router-dom';
4+
import { useTranslation } from 'react-i18next';
25
import { type Organization } from 'app-shared/types/Organization';
36
import { type User } from 'app-shared/types/Repository';
7+
import { useLogoutMutation } from 'app-shared/hooks/mutations/useLogoutMutation';
8+
import { dashboardHeaderMenuItems } from '../../utils/headerUtils/headerUtils';
9+
import { useSelectedContext } from '../../hooks/useSelectedContext';
10+
import { useRepoPath } from '../../hooks/useRepoPath';
11+
import { useSubroute } from '../../hooks/useSubRoute';
12+
import { type NavigationMenuItem } from '../../types/NavigationMenuItem';
13+
import { type NavigationMenuGroup } from '../../types/NavigationMenuGroup';
14+
import type { HeaderMenuItem } from '../../types/HeaderMenuItem';
15+
import { SelectedContextType } from '../../enums/SelectedContextType';
416

5-
export type HeaderContextType = {
17+
export type HeaderContextProps = {
618
selectableOrgs?: Organization[];
719
user: User;
20+
menuItems: HeaderMenuItem[];
21+
profileMenuItems: NavigationMenuItem[];
22+
profileMenuGroups: NavigationMenuGroup[];
823
};
924

10-
export const HeaderContext = createContext<HeaderContextType>({
11-
selectableOrgs: undefined,
12-
user: undefined,
13-
});
25+
export const HeaderContext = createContext<Partial<HeaderContextProps>>(undefined);
26+
27+
export type HeaderContextProviderProps = {
28+
children: ReactNode;
29+
} & HeaderContextProps;
30+
31+
export const HeaderContextProvider = ({
32+
children,
33+
user,
34+
selectableOrgs,
35+
}: Partial<HeaderContextProviderProps>): ReactElement => {
36+
const { t } = useTranslation();
37+
38+
const { mutate: logout } = useLogoutMutation();
39+
const selectedContext = useSelectedContext();
40+
const navigate = useNavigate();
41+
const repoPath = useRepoPath(user, selectableOrgs);
42+
const subroute = useSubroute();
43+
44+
const handleSetSelectedContext = (context: string | SelectedContextType) => {
45+
navigate(`${subroute}/${context}${location.search}`);
46+
};
47+
48+
const allMenuItem: NavigationMenuItem = {
49+
action: { type: 'button', onClick: () => handleSetSelectedContext(SelectedContextType.All) },
50+
itemName: t('shared.header_all'),
51+
isActive: selectedContext === SelectedContextType.All,
52+
};
53+
54+
const selectableOrgMenuItems: NavigationMenuItem[] =
55+
selectableOrgs?.map((selectableOrg: Organization) => ({
56+
action: { type: 'button', onClick: () => handleSetSelectedContext(selectableOrg.username) },
57+
itemName: selectableOrg?.full_name || selectableOrg.username,
58+
isActive: selectedContext === selectableOrg.username,
59+
})) ?? [];
60+
61+
const selfMenuItem: NavigationMenuItem = {
62+
action: { type: 'button', onClick: () => handleSetSelectedContext(SelectedContextType.Self) },
63+
itemName: user?.full_name || user?.login,
64+
isActive: selectedContext === SelectedContextType.Self,
65+
};
66+
67+
const giteaMenuItem: NavigationMenuItem = {
68+
action: { type: 'link', href: repoPath, openInNewTab: true },
69+
itemName: t('shared.header_go_to_gitea'),
70+
};
71+
72+
const logOutMenuItem: NavigationMenuItem = {
73+
action: { type: 'button', onClick: logout },
74+
itemName: t('shared.header_logout'),
75+
};
76+
77+
const selectableOrgMenuGroup: NavigationMenuGroup = {
78+
name: t('dashboard.header_menu_all_orgs'),
79+
showName: true,
80+
items: [allMenuItem, ...selectableOrgMenuItems, selfMenuItem],
81+
};
82+
const profileMenuItems: NavigationMenuItem[] = [giteaMenuItem, logOutMenuItem];
83+
84+
const profileMenuGroups: NavigationMenuGroup[] = [
85+
selectableOrgMenuGroup,
86+
{
87+
name: t('dashboard.header_menu_other'),
88+
showName: false,
89+
items: [giteaMenuItem, logOutMenuItem],
90+
},
91+
];
92+
93+
return (
94+
<HeaderContext.Provider
95+
value={{
96+
user,
97+
selectableOrgs,
98+
menuItems: dashboardHeaderMenuItems.map((item) => ({ name: t(item.name), ...item })),
99+
profileMenuItems,
100+
profileMenuGroups,
101+
}}
102+
>
103+
{children}
104+
</HeaderContext.Provider>
105+
);
106+
};
107+
108+
export const useHeaderContext = (): Partial<HeaderContextProps> => {
109+
const context = useContext(HeaderContext);
110+
if (context === undefined) {
111+
throw new Error('useHeaderContext must be used within a HeaderContextProvider');
112+
}
113+
return context;
114+
};
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export { HeaderContext, type HeaderContextType } from './HeaderContext';
1+
export {
2+
HeaderContextProvider,
3+
HeaderContext,
4+
type HeaderContextProps,
5+
useHeaderContext,
6+
} from './HeaderContext';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export enum HeaderMenuGroupKey {
2+
Overview = 'top_bar.group_overview',
3+
Tools = 'top_bar.group_tools',
4+
Other = 'top_bar.group_other',
5+
}

frontend/dashboard/hooks/usePageHeaderTitle/usePageHeaderTitle.test.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import React from 'react';
22
import { usePageHeaderTitle } from './usePageHeaderTitle';
3-
import { HeaderContext, type HeaderContextType } from 'dashboard/context/HeaderContext';
4-
import { useSelectedContext } from 'dashboard/hooks/useSelectedContext';
5-
import { headerContextValueMock } from 'dashboard/testing/headerContextMock';
3+
import { HeaderContext, type HeaderContextProps } from '../../context/HeaderContext';
4+
import { useSelectedContext } from '../../hooks/useSelectedContext';
5+
import { headerContextValueMock } from '../../testing/headerContextMock';
66
import { SelectedContextType } from '../../enums/SelectedContextType';
7-
import { mockOrg1 } from 'dashboard/testing/organizationMock';
8-
import { renderHookWithProviders } from 'dashboard/testing/mocks';
7+
import { mockOrg1 } from '../../testing/organizationMock';
8+
import { renderHookWithProviders } from '../../testing/mocks';
99

10-
jest.mock('dashboard/hooks/useSelectedContext');
10+
jest.mock('../../hooks/useSelectedContext');
1111

12-
const renderUsePageHeaderTitleHook = (headerContextValueProps: Partial<HeaderContextType> = {}) => {
12+
const renderUsePageHeaderTitleHook = (
13+
headerContextValueProps: Partial<HeaderContextProps> = {},
14+
) => {
1315
return renderHookWithProviders(usePageHeaderTitle, {
1416
externalWrapper: (children) => (
1517
<HeaderContext.Provider value={{ ...headerContextValueMock, ...headerContextValueProps }}>

frontend/dashboard/hooks/usePageHeaderTitle/usePageHeaderTitle.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { useContext } from 'react';
2-
import { HeaderContext } from 'dashboard/context/HeaderContext';
31
import { SelectedContextType } from '../../enums/SelectedContextType';
42
import { getOrgNameByUsername } from 'dashboard/utils/userUtils';
53
import { useSelectedContext } from '../useSelectedContext';
4+
import { useHeaderContext } from '../../context/HeaderContext';
65

76
export const usePageHeaderTitle = () => {
87
const selectedContext = useSelectedContext();
9-
const { selectableOrgs } = useContext(HeaderContext);
8+
const { selectableOrgs } = useHeaderContext();
109

1110
if (selectedContext !== SelectedContextType.All && selectedContext !== SelectedContextType.Self) {
1211
return getOrgNameByUsername(selectedContext, selectableOrgs);

frontend/dashboard/hooks/useProfileMenuTriggerButtonText/useProfileMenuTriggerButtonText.test.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import React from 'react';
22
import { useProfileMenuTriggerButtonText } from './useProfileMenuTriggerButtonText';
3-
import { HeaderContext, type HeaderContextType } from 'dashboard/context/HeaderContext';
3+
import { HeaderContext, type HeaderContextProps } from '../../context/HeaderContext';
44
import { SelectedContextType } from '../../enums/SelectedContextType';
55
import { useSelectedContext } from '../useSelectedContext';
6-
import { userMock } from 'dashboard/testing/userMock';
7-
import { headerContextValueMock } from 'dashboard/testing/headerContextMock';
6+
import { userMock } from '../../testing/userMock';
7+
import { headerContextValueMock } from '../../testing/headerContextMock';
88
import { textMock } from '@studio/testing/mocks/i18nMock';
9-
import { mockOrg1 } from 'dashboard/testing/organizationMock';
10-
import { renderHookWithProviders } from 'dashboard/testing/mocks';
9+
import { mockOrg1 } from '../../testing/organizationMock';
10+
import { renderHookWithProviders } from '../../testing/mocks';
1111

1212
jest.mock('../useSelectedContext');
1313

1414
const renderUseProfileMenuTriggerButtonTextHook = (
15-
headerContextValueProps: Partial<HeaderContextType> = {},
15+
headerContextValueProps: Partial<HeaderContextProps> = {},
1616
) => {
1717
return renderHookWithProviders(useProfileMenuTriggerButtonText, {
1818
externalWrapper: (children) => (

frontend/dashboard/hooks/useProfileMenuTriggerButtonText/useProfileMenuTriggerButtonText.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { useContext } from 'react';
2-
import { HeaderContext } from 'dashboard/context/HeaderContext';
1+
import { useHeaderContext } from '../../context/HeaderContext';
32
import { SelectedContextType } from '../../enums/SelectedContextType';
4-
import { getOrgNameByUsername } from 'dashboard/utils/userUtils';
3+
import { getOrgNameByUsername } from '../../utils/userUtils';
54
import { useTranslation } from 'react-i18next';
65
import { useSelectedContext } from '../useSelectedContext';
76

87
export const useProfileMenuTriggerButtonText = (): string => {
98
const { t } = useTranslation();
10-
const { user, selectableOrgs } = useContext(HeaderContext);
9+
const { user, selectableOrgs } = useHeaderContext();
1110
const selectedContext = useSelectedContext();
1211

1312
const username = user.full_name || user.login;

frontend/dashboard/hooks/useRepoPath/useRepoPath.test.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import React from 'react';
22
import { useRepoPath } from './useRepoPath';
3-
import { HeaderContext, type HeaderContextType } from 'dashboard/context/HeaderContext';
4-
import { useSelectedContext } from 'dashboard/hooks/useSelectedContext';
5-
import { headerContextValueMock } from 'dashboard/testing/headerContextMock';
3+
import { HeaderContext, type HeaderContextProps } from '../../context/HeaderContext';
4+
import { useSelectedContext } from '../../hooks/useSelectedContext';
5+
import { headerContextValueMock } from '../../testing/headerContextMock';
66
import { repositoryOwnerPath, repositoryBasePath } from 'app-shared/api/paths';
7-
import { mockOrg1 } from 'dashboard/testing/organizationMock';
8-
import { userMock } from 'dashboard/testing/userMock';
9-
import { renderHookWithProviders } from 'dashboard/testing/mocks';
7+
import { mockOrg1, mockOrganizations } from '../../testing/organizationMock';
8+
import { userMock } from '../../testing/userMock';
9+
import { renderHookWithProviders } from '../../testing/mocks';
10+
import type { User } from 'app-shared/types/Repository';
1011

1112
jest.mock('dashboard/hooks/useSelectedContext');
1213

13-
const renderUseRepoPathHook = (headerContextValueProps: Partial<HeaderContextType> = {}) => {
14-
return renderHookWithProviders(useRepoPath, {
14+
type Props = {
15+
headerContextValueProps: Partial<HeaderContextProps>;
16+
user: User;
17+
};
18+
const renderUseRepoPathHook = (props: Partial<Props> = {}) => {
19+
const { headerContextValueProps, user = userMock } = props;
20+
return renderHookWithProviders(() => useRepoPath(user, mockOrganizations), {
1521
externalWrapper: (children) => (
1622
<HeaderContext.Provider value={{ ...headerContextValueMock, ...headerContextValueProps }}>
1723
{children}

frontend/dashboard/hooks/useRepoPath/useRepoPath.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { useContext } from 'react';
21
import { repositoryBasePath, repositoryOwnerPath } from 'app-shared/api/paths';
32
import { getOrgUsernameByUsername } from 'dashboard/utils/userUtils';
43
import { useSelectedContext } from 'dashboard/hooks/useSelectedContext';
5-
import { HeaderContext } from 'dashboard/context/HeaderContext';
6-
7-
export const useRepoPath = () => {
8-
const { user, selectableOrgs } = useContext(HeaderContext);
4+
import type { User } from 'app-shared/types/Repository';
5+
import type { Organization } from 'app-shared/types/Organization';
96

7+
export const useRepoPath = (user: User, selectableOrgs: Organization[]) => {
108
const selectedContext = useSelectedContext();
119
const org = getOrgUsernameByUsername(selectedContext, selectableOrgs);
1210

0 commit comments

Comments
 (0)