Skip to content

Commit 0859b0e

Browse files
authored
Merge branch 'spr/main/cf35ff23' into spr/main/d6447eac
2 parents 0fc87ee + 9050792 commit 0859b0e

File tree

10 files changed

+243
-18
lines changed

10 files changed

+243
-18
lines changed

backend/src/Designer/EventHandlers/LayoutPageAdded/LayoutPageAddedHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ await _fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotific
3838
TextResource jsonTexts = await repository.GetTextV1("nb");
3939
int initialCount = jsonTexts.Resources.Count;
4040
AddTextResourceIfNotExists(jsonTexts.Resources, "next", "Neste");
41-
AddTextResourceIfNotExists(jsonTexts.Resources, "back", "Tilbake");
41+
AddTextResourceIfNotExists(jsonTexts.Resources, "back", "Forrige");
4242

4343
if (jsonTexts.Resources.Count != initialCount)
4444
{

frontend/app-development/router/routes.test.tsx

+102-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { render, screen } from '@testing-library/react';
2-
import { routerRoutes } from './routes';
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import { routerRoutes, UiEditor } from './routes';
33
import { RoutePaths } from '../enums/RoutePaths';
44
import React from 'react';
55
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
@@ -13,6 +13,9 @@ import { PreviewContextProvider } from '../contexts/PreviewContext';
1313
import { AppDevelopmentContextProvider } from '../contexts/AppDevelopmentContext';
1414
import { textMock } from '@studio/testing/mocks/i18nMock';
1515
import type { QueryClient } from '@tanstack/react-query';
16+
import { LayoutContext } from 'app-development/contexts/LayoutContext/LayoutContext';
17+
import { SubApp as UiEditorLatest } from '@altinn/ux-editor/SubApp';
18+
import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
1619

1720
// Mocks:
1821
jest.mock('@altinn/ux-editor-v3/SubApp', () => ({
@@ -22,8 +25,79 @@ jest.mock('@altinn/ux-editor/SubApp', () => ({
2225
SubApp: () => <div data-testid='latest version' />,
2326
}));
2427

28+
jest.mock('@altinn/ux-editor/SubApp', () => ({
29+
SubApp: ({ onLayoutSetNameChange }: { onLayoutSetNameChange: (name: string) => void }) => {
30+
React.useEffect(() => {
31+
onLayoutSetNameChange('test-layout');
32+
}, [onLayoutSetNameChange]);
33+
return <div data-testid='latest version' />;
34+
},
35+
}));
36+
37+
jest.mock('@altinn/ux-editor/containers/FormDesignNavigation', () => ({
38+
FormDesignerNavigation: () => <div>Form Designer Navigation</div>,
39+
}));
40+
41+
jest.mock('app-shared/utils/featureToggleUtils', () => ({
42+
...jest.requireActual('app-shared/utils/featureToggleUtils'),
43+
shouldDisplayFeature: jest.fn(),
44+
}));
45+
46+
const renderWithProviders = (
47+
ui: React.ReactElement,
48+
queryClient: QueryClient,
49+
layoutContextValue?: any,
50+
) => {
51+
return render(
52+
<ServicesContextProvider {...queriesMock} client={queryClient}>
53+
<SettingsModalContextProvider>
54+
<PreviewContextProvider>
55+
<AppDevelopmentContextProvider>
56+
<LayoutContext.Provider value={layoutContextValue}>{ui}</LayoutContext.Provider>
57+
</AppDevelopmentContextProvider>
58+
</PreviewContextProvider>
59+
</SettingsModalContextProvider>
60+
</ServicesContextProvider>,
61+
);
62+
};
63+
2564
describe('routes', () => {
2665
describe(RoutePaths.UIEditor, () => {
66+
afterEach(() => {
67+
jest.restoreAllMocks();
68+
});
69+
it('calls setSelectedLayoutSetName when onLayoutSetNameChange is triggered', () => {
70+
const setSelectedLayoutSetName = jest.fn();
71+
const queryClient = createQueryClientMock();
72+
const appVersion: AppVersion = {
73+
frontendVersion: '4.0.0',
74+
backendVersion: '7.0.0',
75+
};
76+
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
77+
const { rerender } = renderWithProviders(<UiEditor />, queryClient, {
78+
setSelectedLayoutSetName,
79+
});
80+
const layoutSetName = 'test-layout';
81+
const onLayoutSetNameChange = jest.fn();
82+
rerender(
83+
<UiEditorLatest
84+
shouldReloadPreview={false}
85+
previewHasLoaded={undefined}
86+
onLayoutSetNameChange={onLayoutSetNameChange}
87+
/>,
88+
);
89+
onLayoutSetNameChange(layoutSetName);
90+
expect(setSelectedLayoutSetName).toHaveBeenCalledWith(layoutSetName);
91+
});
92+
93+
it('Returns null when there is no AppVersion', async () => {
94+
const queryClient = createQueryClientMock();
95+
queryClient.setQueryData([QueryKey.AppVersion, org, app], null);
96+
renderUiEditor(queryClient);
97+
expect(screen.queryByTestId('version 3')).not.toBeInTheDocument();
98+
expect(screen.queryByTestId('latest version')).not.toBeInTheDocument();
99+
});
100+
27101
type FrontendVersion = null | '3.0.0' | '4.0.0';
28102
type PackageVersion = 'version 3' | 'latest version';
29103
type TestCase = [PackageVersion, FrontendVersion];
@@ -36,23 +110,47 @@ describe('routes', () => {
36110

37111
it.each(testCases)(
38112
'Renders the %s schema editor page when the app frontend version is %s',
39-
(expectedPackage, frontendVersion) => {
113+
async (expectedPackage, frontendVersion) => {
40114
const appVersion: AppVersion = {
41115
frontendVersion,
42116
backendVersion: '7.0.0',
43117
};
44118
const queryClient = createQueryClientMock();
45119
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
46120
renderUiEditor(queryClient);
47-
expect(screen.getByTestId(expectedPackage)).toBeInTheDocument();
121+
expect(await screen.findByTestId(expectedPackage)).toBeInTheDocument();
48122
},
49123
);
50124

125+
it('renders a loading spinner', async () => {
126+
renderUiEditor();
127+
await waitFor(() => {
128+
expect(screen.getByTestId('studio-spinner-test-id')).toBeInTheDocument();
129+
});
130+
});
131+
51132
it('renders a loading spinner while fetching frontend version', () => {
52133
renderUiEditor();
53134
expect(screen.getByText(textMock('ux_editor.loading_page'))).toBeInTheDocument();
54135
});
55136

137+
it('renders FormDesignerNavigation when task navigation is enabled and no layout set is selected', () => {
138+
(shouldDisplayFeature as jest.Mock).mockImplementation(
139+
(feature) => feature === FeatureFlag.TaskNavigation,
140+
);
141+
const queryClient = createQueryClientMock();
142+
queryClient.setQueryData([QueryKey.AppVersion, org, app], {
143+
frontendVersion: '4.0.0',
144+
backendVersion: '7.0.0',
145+
});
146+
147+
renderWithProviders(<UiEditor />, queryClient, {
148+
setSelectedLayoutSetName: jest.fn(),
149+
selectedFormLayoutSetName: undefined,
150+
});
151+
expect(screen.getByText('Form Designer Navigation')).toBeInTheDocument();
152+
});
153+
56154
const renderUiEditor = (queryClient: QueryClient = createQueryClientMock()) =>
57155
renderSubapp(RoutePaths.UIEditor, queryClient);
58156
});

frontend/app-development/router/routes.tsx

+25-11
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import { useAppVersionQuery } from 'app-shared/hooks/queries';
1212
import React from 'react';
1313
import { usePreviewContext } from '../contexts/PreviewContext';
1414
import { useLayoutContext } from '../contexts/LayoutContext';
15-
import { StudioPageSpinner } from '@studio/components';
15+
import { StudioPageSpinner, useLocalStorage } from '@studio/components';
1616
import { useTranslation } from 'react-i18next';
1717
import { AppContentLibrary } from 'app-development/features/appContentLibrary';
18+
import { FormDesignerNavigation } from '@altinn/ux-editor/containers/FormDesignNavigation';
19+
import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
1820

1921
interface IRouteProps {
2022
headerTextKey?: string;
@@ -39,28 +41,40 @@ const latestFrontendVersion = '4';
3941
const isLatestFrontendVersion = (version: AppVersion): boolean =>
4042
version?.frontendVersion?.startsWith(latestFrontendVersion);
4143

42-
const UiEditor = () => {
44+
export const UiEditor = () => {
4345
const { org, app } = useStudioEnvironmentParams();
4446
const { t } = useTranslation();
4547
const { data: version, isPending: fetchingVersionIsPending } = useAppVersionQuery(org, app);
4648
const { shouldReloadPreview, previewHasLoaded } = usePreviewContext();
4749
const { setSelectedLayoutSetName } = useLayoutContext();
50+
const [selectedFormLayoutSetName] = useLocalStorage<string>('layoutSet/' + app);
51+
const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation);
4852

4953
if (fetchingVersionIsPending) {
5054
return <StudioPageSpinner spinnerTitle={t('ux_editor.loading_page')} />;
5155
}
5256

5357
if (!version) return null;
5458

55-
return isLatestFrontendVersion(version) ? (
56-
<UiEditorLatest
57-
shouldReloadPreview={shouldReloadPreview}
58-
previewHasLoaded={previewHasLoaded}
59-
onLayoutSetNameChange={(layoutSetName) => setSelectedLayoutSetName(layoutSetName)}
60-
/>
61-
) : (
62-
<UiEditorV3 />
63-
);
59+
const renderUiEditorContent = () => {
60+
if (isTaskNavigationEnabled && !selectedFormLayoutSetName) {
61+
return <FormDesignerNavigation />;
62+
}
63+
64+
const handleLayoutSetNameChange = (layoutSetName: string) => {
65+
setSelectedLayoutSetName(layoutSetName);
66+
};
67+
68+
return (
69+
<UiEditorLatest
70+
shouldReloadPreview={shouldReloadPreview}
71+
previewHasLoaded={previewHasLoaded}
72+
onLayoutSetNameChange={handleLayoutSetNameChange}
73+
/>
74+
);
75+
};
76+
77+
return isLatestFrontendVersion(version) ? renderUiEditorContent() : <UiEditorV3 />;
6478
};
6579

6680
export const routerRoutes: RouterRoute[] = [

frontend/packages/shared/src/utils/featureToggleUtils.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ describe('featureToggle localStorage', () => {
2727
it('should return false if feature is not enabled in the localStorage', () => {
2828
expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy();
2929
});
30+
31+
it('should return true if TaskNavigation is enabled in the localStorage', () => {
32+
typedLocalStorage.setItem<string[]>('featureFlags', ['taskNavigation']);
33+
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeTruthy();
34+
});
35+
36+
it('should return false if TaskNavigation is not enabled in the localStorage', () => {
37+
typedLocalStorage.setItem<string[]>('featureFlags', ['demo']);
38+
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeFalsy();
39+
});
3040
});
3141

3242
describe('featureToggle url', () => {
@@ -69,6 +79,16 @@ describe('featureToggle url', () => {
6979
]);
7080
expect(typedLocalStorage.getItem<string[]>('featureFlags')).toBeNull();
7181
});
82+
83+
it('should return true if TaskNavigation is enabled in the url', () => {
84+
window.history.pushState({}, 'PageUrl', '/?featureFlags=taskNavigation');
85+
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeTruthy();
86+
});
87+
88+
it('should return false if TaskNavigation is not enabled in the url', () => {
89+
window.history.pushState({}, 'PageUrl', '/?featureFlags=demo');
90+
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeFalsy();
91+
});
7292
});
7393

7494
describe('addFeatureToLocalStorage', () => {
@@ -89,6 +109,11 @@ describe('addFeatureToLocalStorage', () => {
89109
'shouldOverrideAppLibCheck',
90110
]);
91111
});
112+
113+
it('should add TaskNavigation to local storage', () => {
114+
addFeatureFlagToLocalStorage(FeatureFlag.TaskNavigation);
115+
expect(typedLocalStorage.getItem<string[]>('featureFlags')).toEqual(['taskNavigation']);
116+
});
92117
});
93118

94119
describe('removeFeatureFromLocalStorage', () => {

frontend/packages/shared/src/utils/featureToggleUtils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export enum FeatureFlag {
1111
MainConfig = 'mainConfig',
1212
OptionListEditor = 'optionListEditor',
1313
ShouldOverrideAppLibCheck = 'shouldOverrideAppLibCheck',
14+
TaskNavigation = 'taskNavigation',
1415
}
1516

1617
/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.wrapper {
2+
background-color: #e6eff8;
3+
}
4+
5+
.container {
6+
margin: auto;
7+
padding: var(--fds-spacing-6);
8+
position: relative;
9+
}
10+
11+
.header {
12+
color: #022f51;
13+
font-family: var(--studio-font-family);
14+
font-size: var(--fds-sizing-9);
15+
font-weight: 500;
16+
}
17+
18+
.panel {
19+
background-color: white;
20+
border-radius: var(--fds-border_radius-medium);
21+
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.05);
22+
padding: var(--fds-spacing-5);
23+
position: relative;
24+
margin: var(--fds-spacing-5);
25+
}
26+
27+
.content {
28+
display: flex;
29+
flex-direction: column;
30+
gap: var(--fds-spacing-10);
31+
}
32+
33+
.footer {
34+
border-top: 2px solid #efefef;
35+
margin-top: var(--fds-spacing-10);
36+
padding-top: var(--fds-spacing-8);
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
import { FormDesignerNavigation } from './FormDesignerNavigation';
4+
import { renderWithProviders } from 'app-development/test/mocks';
5+
import { textMock } from '@studio/testing/mocks/i18nMock';
6+
7+
jest.mock('app-development/hooks/queries', () => ({
8+
useAppConfigQuery: jest.fn(() => ({ data: { serviceName: 'test' } })),
9+
}));
10+
11+
describe('FormDesignerNavigation', () => {
12+
it('renders the component with heading text test', () => {
13+
render();
14+
expect(screen.getByText('test')).toBeInTheDocument();
15+
});
16+
17+
it('renders the contact link', () => {
18+
render();
19+
expect(screen.getByRole('link', { name: textMock('general.contact') })).toBeInTheDocument();
20+
});
21+
});
22+
23+
const render = () => renderWithProviders()(<FormDesignerNavigation />);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Link } from '@digdir/designsystemet-react';
2+
import React from 'react';
3+
import classes from './FormDesignerNavigation.module.css';
4+
import { useTranslation } from 'react-i18next';
5+
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
6+
import { useAppConfigQuery } from 'app-development/hooks/queries';
7+
8+
export const FormDesignerNavigation = () => {
9+
const { t } = useTranslation();
10+
const { org, app } = useStudioEnvironmentParams();
11+
const { data: appConfigData } = useAppConfigQuery(org, app);
12+
return (
13+
<div className={classes.wrapper}>
14+
<main className={classes.container}>
15+
<div className={classes.panel}>
16+
<div className={classes.content}>
17+
<div className={classes.header}>{appConfigData?.serviceName}</div>
18+
</div>
19+
<footer className={classes.footer}>
20+
<Link href='/contact'>{t('general.contact')}</Link>
21+
</footer>
22+
</div>
23+
</main>
24+
</div>
25+
);
26+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FormDesignerNavigation } from './FormDesignerNavigation';

src/Altinn.Platform/Altinn.Platform.PDF/pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<parent>
3333
<groupId>org.springframework.boot</groupId>
3434
<artifactId>spring-boot-starter-parent</artifactId>
35-
<version>3.4.2</version>
35+
<version>3.4.3</version>
3636
</parent>
3737
<dependencies>
3838
<dependency>
@@ -78,7 +78,7 @@
7878
<dependency>
7979
<groupId>com.azure</groupId>
8080
<artifactId>azure-identity</artifactId>
81-
<version>1.15.2</version>
81+
<version>1.15.3</version>
8282
</dependency>
8383
<dependency>
8484
<groupId>org.springframework.boot</groupId>

0 commit comments

Comments
 (0)