From 43ab4a32a62d7a082cb2f12cadb1e0b946349184 Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Wed, 5 Mar 2025 14:55:42 +0100 Subject: [PATCH 1/7] Retrieve layout set name from AppContext --- frontend/app-development/router/routes.tsx | 10 +--------- .../src/hooks/useSelectedFormLayoutSetName.ts | 11 +++++++---- frontend/packages/ux-editor/src/AppContext.tsx | 10 ++++++++-- frontend/packages/ux-editor/src/SubApp.tsx | 17 +++++++++++++++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/frontend/app-development/router/routes.tsx b/frontend/app-development/router/routes.tsx index 9f9cd3fa468..1d3a47bcca8 100644 --- a/frontend/app-development/router/routes.tsx +++ b/frontend/app-development/router/routes.tsx @@ -12,11 +12,9 @@ import { useAppVersionQuery } from 'app-shared/hooks/queries'; import React from 'react'; import { usePreviewContext } from '../contexts/PreviewContext'; import { useLayoutContext } from '../contexts/LayoutContext'; -import { StudioPageSpinner, useLocalStorage } from '@studio/components'; +import { StudioPageSpinner } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { AppContentLibrary } from 'app-development/features/appContentLibrary'; -import { FormDesignerNavigation } from '@altinn/ux-editor/containers/FormDesignNavigation'; -import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; interface IRouteProps { headerTextKey?: string; @@ -47,8 +45,6 @@ export const UiEditor = () => { const { data: version, isPending: fetchingVersionIsPending } = useAppVersionQuery(org, app); const { shouldReloadPreview, previewHasLoaded } = usePreviewContext(); const { setSelectedLayoutSetName } = useLayoutContext(); - const [selectedFormLayoutSetName] = useLocalStorage('layoutSet/' + app); - const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); if (fetchingVersionIsPending) { return ; @@ -57,10 +53,6 @@ export const UiEditor = () => { if (!version) return null; const renderUiEditorContent = () => { - if (isTaskNavigationEnabled && !selectedFormLayoutSetName) { - return ; - } - const handleLayoutSetNameChange = (layoutSetName: string) => { setSelectedLayoutSetName(layoutSetName); }; diff --git a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.ts b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.ts index 9b0ccef8a88..49bdd895e3f 100644 --- a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.ts +++ b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.ts @@ -1,27 +1,30 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage'; import { type LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; +import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export type UseSelectedFormLayoutSetNameResult = { selectedFormLayoutSetName: string; setSelectedFormLayoutSetName: (layoutName: string) => void; + removeSelectedFormLayoutSetName: () => void; }; export const useSelectedFormLayoutSetName = ( layoutSets: LayoutSets, ): UseSelectedFormLayoutSetNameResult => { const { app } = useStudioEnvironmentParams(); + const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); - const defaultLayoutSet = layoutSets?.sets[0]?.id ?? ''; + const defaultLayoutSet = isTaskNavigationEnabled ? undefined : (layoutSets?.sets[0]?.id ?? ''); - const [selectedFormLayoutSetName, setSelectedFormLayoutSetName] = useLocalStorage( - 'layoutSet/' + app, - ); + const [selectedFormLayoutSetName, setSelectedFormLayoutSetName, removeSelectedFormLayoutSetName] = + useLocalStorage('layoutSet/' + app); const layoutSetExists = layoutSets?.sets.some((set) => set.id === selectedFormLayoutSetName); return { selectedFormLayoutSetName: layoutSetExists ? selectedFormLayoutSetName : defaultLayoutSet, setSelectedFormLayoutSetName, + removeSelectedFormLayoutSetName, }; }; diff --git a/frontend/packages/ux-editor/src/AppContext.tsx b/frontend/packages/ux-editor/src/AppContext.tsx index f748c953a66..27827f3b50f 100644 --- a/frontend/packages/ux-editor/src/AppContext.tsx +++ b/frontend/packages/ux-editor/src/AppContext.tsx @@ -17,6 +17,7 @@ export interface AppContextProps { previewIframeRef: MutableRefObject; selectedFormLayoutSetName: string; setSelectedFormLayoutSetName: (selectedFormLayoutSetName: string) => void; + removeSelectedFormLayoutSetName: () => void; selectedFormLayoutName: string; setSelectedFormLayoutName: (selectedFormLayoutName: string) => void; updateLayoutsForPreview: (layoutSetName: string, resetQueries?: boolean) => Promise; @@ -48,8 +49,11 @@ export const AppContextProvider = ({ const { org, app } = useStudioEnvironmentParams(); const { data: layoutSets, isPending: pendingLayoutsets } = useLayoutSetsQuery(org, app); - const { selectedFormLayoutSetName, setSelectedFormLayoutSetName } = - useSelectedFormLayoutSetName(layoutSets); + const { + selectedFormLayoutSetName, + setSelectedFormLayoutSetName, + removeSelectedFormLayoutSetName, + } = useSelectedFormLayoutSetName(layoutSets); const { selectedFormLayoutName, setSelectedFormLayoutName } = useSelectedFormLayoutName(selectedFormLayoutSetName); @@ -102,6 +106,7 @@ export const AppContextProvider = ({ previewIframeRef, selectedFormLayoutSetName, setSelectedFormLayoutSetName, + removeSelectedFormLayoutSetName, selectedFormLayoutName, setSelectedFormLayoutName, updateLayoutsForPreview, @@ -117,6 +122,7 @@ export const AppContextProvider = ({ setSelectedFormLayoutSetName, selectedFormLayoutName, setSelectedFormLayoutName, + removeSelectedFormLayoutSetName, updateLayoutsForPreview, updateLayoutSetsForPreview, updateLayoutSettingsForPreview, diff --git a/frontend/packages/ux-editor/src/SubApp.tsx b/frontend/packages/ux-editor/src/SubApp.tsx index 2cf52fe6523..fa66ba0b55c 100644 --- a/frontend/packages/ux-editor/src/SubApp.tsx +++ b/frontend/packages/ux-editor/src/SubApp.tsx @@ -1,7 +1,10 @@ import React from 'react'; -import { App } from './App'; import './styles/index.css'; import { AppContextProvider } from './AppContext'; +import { App } from './App'; +import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { FormDesignerNavigation } from '@altinn/ux-editor/containers/FormDesignNavigation'; +import { useAppContext } from './hooks'; type SubAppProps = { shouldReloadPreview: boolean; @@ -10,9 +13,19 @@ type SubAppProps = { }; export const SubApp = (props: SubAppProps) => { + const UiEditor = () => { + const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); + const { selectedFormLayoutSetName } = useAppContext(); + + return isTaskNavigationEnabled && !selectedFormLayoutSetName ? ( + + ) : ( + + ); + }; return ( - + ); }; From 5c4d9ac34b6de0328f11473693148cfb79dc4a52 Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Wed, 5 Mar 2025 14:56:18 +0100 Subject: [PATCH 2/7] Fix tests --- .../packages/ux-editor/src/SubApp.test.tsx | 20 ++++++++++++------- frontend/packages/ux-editor/src/SubApp.tsx | 2 +- .../ux-editor/src/testing/appContextMock.ts | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/packages/ux-editor/src/SubApp.test.tsx b/frontend/packages/ux-editor/src/SubApp.test.tsx index 6ce62dc867a..bf573d6f703 100644 --- a/frontend/packages/ux-editor/src/SubApp.test.tsx +++ b/frontend/packages/ux-editor/src/SubApp.test.tsx @@ -2,14 +2,26 @@ import type { ReactNode } from 'react'; import React from 'react'; import { SubApp } from './SubApp'; import { render, screen, within } from '@testing-library/react'; +import { appContextMock } from './testing/appContextMock'; const providerTestId = 'provider'; const appTestId = 'app'; +const formNavigationTestId = 'formNavigation'; jest.mock('./AppContext', () => ({ AppContextProvider: ({ children }: { children: ReactNode }) => { return
{children}
; }, })); +jest.mock('./hooks', () => ({ + useAppContext: () => { + return {}; + }, +})); +jest.mock('./containers/FormDesignNavigation', () => ({ + FormDesignerNavigation: () => { + return
FormDesignerNavigation
; + }, +})); jest.mock('./App', () => ({ App: () => { return
App
; @@ -18,13 +30,7 @@ jest.mock('./App', () => ({ describe('SubApp', () => { it('Renders the app within the AppContext provider', () => { - render( - , - ); + render(); const provider = screen.getByTestId(providerTestId); expect(provider).toBeInTheDocument(); expect(within(provider).getByTestId(appTestId)).toBeInTheDocument(); diff --git a/frontend/packages/ux-editor/src/SubApp.tsx b/frontend/packages/ux-editor/src/SubApp.tsx index fa66ba0b55c..e8e94aa3a9a 100644 --- a/frontend/packages/ux-editor/src/SubApp.tsx +++ b/frontend/packages/ux-editor/src/SubApp.tsx @@ -3,7 +3,7 @@ import './styles/index.css'; import { AppContextProvider } from './AppContext'; import { App } from './App'; import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; -import { FormDesignerNavigation } from '@altinn/ux-editor/containers/FormDesignNavigation'; +import { FormDesignerNavigation } from './containers/FormDesignNavigation'; import { useAppContext } from './hooks'; type SubAppProps = { diff --git a/frontend/packages/ux-editor/src/testing/appContextMock.ts b/frontend/packages/ux-editor/src/testing/appContextMock.ts index 8224645d412..ca0d3863066 100644 --- a/frontend/packages/ux-editor/src/testing/appContextMock.ts +++ b/frontend/packages/ux-editor/src/testing/appContextMock.ts @@ -11,6 +11,7 @@ export const appContextMock: AppContextProps = { previewIframeRef: previewIframeRefMock, selectedFormLayoutSetName: layoutSet1NameMock, setSelectedFormLayoutSetName: jest.fn(), + removeSelectedFormLayoutSetName: jest.fn(), selectedFormLayoutName: layout1NameMock, setSelectedFormLayoutName: jest.fn(), updateLayoutSetsForPreview: jest.fn(), From 5592a2fcf5fa3e8496df4303d6efeaeae714f5ae Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Thu, 6 Mar 2025 10:46:58 +0100 Subject: [PATCH 3/7] Fix tests --- .../app-development/router/routes.test.tsx | 27 ------------- .../packages/ux-editor/src/SubApp.test.tsx | 39 ++++++++++++++++++- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/frontend/app-development/router/routes.test.tsx b/frontend/app-development/router/routes.test.tsx index 0fe44329d79..8adb2ea74b3 100644 --- a/frontend/app-development/router/routes.test.tsx +++ b/frontend/app-development/router/routes.test.tsx @@ -15,7 +15,6 @@ import { textMock } from '@studio/testing/mocks/i18nMock'; import type { QueryClient } from '@tanstack/react-query'; import { LayoutContext } from 'app-development/contexts/LayoutContext/LayoutContext'; import { SubApp as UiEditorLatest } from '@altinn/ux-editor/SubApp'; -import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; // Mocks: jest.mock('@altinn/ux-editor-v3/SubApp', () => ({ @@ -34,15 +33,6 @@ jest.mock('@altinn/ux-editor/SubApp', () => ({ }, })); -jest.mock('@altinn/ux-editor/containers/FormDesignNavigation', () => ({ - FormDesignerNavigation: () =>
Form Designer Navigation
, -})); - -jest.mock('app-shared/utils/featureToggleUtils', () => ({ - ...jest.requireActual('app-shared/utils/featureToggleUtils'), - shouldDisplayFeature: jest.fn(), -})); - const renderWithProviders = ( ui: React.ReactElement, queryClient: QueryClient, @@ -134,23 +124,6 @@ describe('routes', () => { expect(screen.getByText(textMock('ux_editor.loading_page'))).toBeInTheDocument(); }); - it('renders FormDesignerNavigation when task navigation is enabled and no layout set is selected', () => { - (shouldDisplayFeature as jest.Mock).mockImplementation( - (feature) => feature === FeatureFlag.TaskNavigation, - ); - const queryClient = createQueryClientMock(); - queryClient.setQueryData([QueryKey.AppVersion, org, app], { - frontendVersion: '4.0.0', - backendVersion: '7.0.0', - }); - - renderWithProviders(, queryClient, { - setSelectedLayoutSetName: jest.fn(), - selectedFormLayoutSetName: undefined, - }); - expect(screen.getByText('Form Designer Navigation')).toBeInTheDocument(); - }); - const renderUiEditor = (queryClient: QueryClient = createQueryClientMock()) => renderSubapp(RoutePaths.UIEditor, queryClient); }); diff --git a/frontend/packages/ux-editor/src/SubApp.test.tsx b/frontend/packages/ux-editor/src/SubApp.test.tsx index bf573d6f703..7059f691aea 100644 --- a/frontend/packages/ux-editor/src/SubApp.test.tsx +++ b/frontend/packages/ux-editor/src/SubApp.test.tsx @@ -3,6 +3,13 @@ import React from 'react'; import { SubApp } from './SubApp'; import { render, screen, within } from '@testing-library/react'; import { appContextMock } from './testing/appContextMock'; +import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import type { QueryClient } from '@tanstack/react-query'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { app, org } from '@studio/testing/testids'; const providerTestId = 'provider'; const appTestId = 'app'; @@ -19,7 +26,7 @@ jest.mock('./hooks', () => ({ })); jest.mock('./containers/FormDesignNavigation', () => ({ FormDesignerNavigation: () => { - return
FormDesignerNavigation
; + return
Form Designer Navigation
; }, })); jest.mock('./App', () => ({ @@ -28,11 +35,39 @@ jest.mock('./App', () => ({ }, })); +jest.mock('app-shared/utils/featureToggleUtils', () => ({ + ...jest.requireActual('app-shared/utils/featureToggleUtils'), + shouldDisplayFeature: jest.fn(), +})); + describe('SubApp', () => { it('Renders the app within the AppContext provider', () => { - render(); + renderWithProviders(); const provider = screen.getByTestId(providerTestId); expect(provider).toBeInTheDocument(); expect(within(provider).getByTestId(appTestId)).toBeInTheDocument(); }); + + it('renders FormDesignerNavigation when task navigation is enabled and no layout set is selected', () => { + (shouldDisplayFeature as jest.Mock).mockImplementation( + (feature) => feature === FeatureFlag.TaskNavigation, + ); + const queryClient = createQueryClientMock(); + queryClient.setQueryData([QueryKey.AppVersion, org, app], { + frontendVersion: '4.0.0', + backendVersion: '7.0.0', + }); + renderWithProviders(queryClient); + const provider = screen.getByTestId(providerTestId); + expect(provider).toBeInTheDocument(); + expect(within(provider).getByTestId(formNavigationTestId)).toBeInTheDocument(); + }); }); + +const renderWithProviders = (queryClient?: QueryClient) => { + return render( + + + , + ); +}; From 84b7cd1c45d99196d6dd5e6cfbaaeefafa919b92 Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Mon, 10 Mar 2025 09:20:55 +0100 Subject: [PATCH 4/7] Add test for default layoutSet --- .../useSelectedFormLayoutSetName.test.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx index 7114a476b87..3db69926707 100644 --- a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx +++ b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx @@ -9,6 +9,8 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; import type { QueryClient } from '@tanstack/react-query'; import { useSelectedFormLayoutSetName } from './useSelectedFormLayoutSetName'; import { type LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; +import { typedLocalStorage } from '@studio/pure-functions'; +import { FeatureFlag } from 'app-shared/utils/featureToggleUtils'; // Test data: export const layoutSet1NameMock = 'test-layout-set'; @@ -51,7 +53,10 @@ const wrapper = ({ }; describe('useSelectedFormLayoutSetName', () => { - afterEach(jest.clearAllMocks); + afterEach(() => { + typedLocalStorage.removeItem('featureFlags'); + jest.clearAllMocks; + }); it('should return empty string when there are no layout sets', async () => { const { result } = renderHook(() => useSelectedFormLayoutSetName(undefined), { wrapper }); @@ -69,6 +74,18 @@ describe('useSelectedFormLayoutSetName', () => { expect(result.current.selectedFormLayoutSetName).toEqual(layoutSetsMock.sets[0].id); }); + it('should return undefined when selected layout does not exist and taskNavigation feature flag is enabled', async () => { + const client = createQueryClientMock(); + typedLocalStorage.setItem('featureFlags', [FeatureFlag.TaskNavigation]); + + const { result } = renderHook(() => useSelectedFormLayoutSetName(layoutSetsMock), { + wrapper: ({ children }) => { + return wrapper({ children, client }); + }, + }); + expect(result.current.selectedFormLayoutSetName).toEqual(undefined); + }); + it('should return selected layout set when selected does exist', async () => { const client = createQueryClientMock(); From 3125af1b90728d4936cdce0bd1de066bb91bc23c Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Mon, 10 Mar 2025 10:24:28 +0100 Subject: [PATCH 5/7] Fix tests --- .../shared/src/hooks/useSelectedFormLayoutSetName.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx index 3db69926707..829883bffe0 100644 --- a/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx +++ b/frontend/packages/shared/src/hooks/useSelectedFormLayoutSetName.test.tsx @@ -55,7 +55,7 @@ const wrapper = ({ describe('useSelectedFormLayoutSetName', () => { afterEach(() => { typedLocalStorage.removeItem('featureFlags'); - jest.clearAllMocks; + jest.clearAllMocks(); }); it('should return empty string when there are no layout sets', async () => { From d88427282dbc358ba3ec631d12ab9c0f9ac67bc2 Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Tue, 11 Mar 2025 18:05:36 +0100 Subject: [PATCH 6/7] Fix rerender --- frontend/packages/ux-editor/src/SubApp.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/packages/ux-editor/src/SubApp.tsx b/frontend/packages/ux-editor/src/SubApp.tsx index e8e94aa3a9a..e5d1346221b 100644 --- a/frontend/packages/ux-editor/src/SubApp.tsx +++ b/frontend/packages/ux-editor/src/SubApp.tsx @@ -12,17 +12,18 @@ type SubAppProps = { onLayoutSetNameChange: (layoutSetName: string) => void; }; -export const SubApp = (props: SubAppProps) => { - const UiEditor = () => { - const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); - const { selectedFormLayoutSetName } = useAppContext(); +const UiEditor = () => { + const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); + const { selectedFormLayoutSetName } = useAppContext(); - return isTaskNavigationEnabled && !selectedFormLayoutSetName ? ( - - ) : ( - - ); - }; + return isTaskNavigationEnabled && !selectedFormLayoutSetName ? ( + + ) : ( + + ); +}; + +export const SubApp = (props: SubAppProps) => { return ( From 18495fb1376c8b6a1375d7ea0b98d522f3470af1 Mon Sep 17 00:00:00 2001 From: Michael Queyrichon Date: Tue, 11 Mar 2025 18:15:41 +0100 Subject: [PATCH 7/7] Update names --- frontend/packages/ux-editor/src/SubApp.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/packages/ux-editor/src/SubApp.tsx b/frontend/packages/ux-editor/src/SubApp.tsx index e5d1346221b..22af2d3c5f8 100644 --- a/frontend/packages/ux-editor/src/SubApp.tsx +++ b/frontend/packages/ux-editor/src/SubApp.tsx @@ -1,7 +1,7 @@ import React from 'react'; import './styles/index.css'; import { AppContextProvider } from './AppContext'; -import { App } from './App'; +import { App as FormDesigner } from './App'; import { FeatureFlag, shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; import { FormDesignerNavigation } from './containers/FormDesignNavigation'; import { useAppContext } from './hooks'; @@ -12,21 +12,21 @@ type SubAppProps = { onLayoutSetNameChange: (layoutSetName: string) => void; }; -const UiEditor = () => { +const App = () => { const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation); const { selectedFormLayoutSetName } = useAppContext(); return isTaskNavigationEnabled && !selectedFormLayoutSetName ? ( ) : ( - + ); }; export const SubApp = (props: SubAppProps) => { return ( - + ); };