Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 14732 set up a new navigation page #14804

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e1cf07d
added feature flag and new component FormDesignernavigation
JamalAlabdullah Feb 25, 2025
6c093c3
added test for featureFlags
JamalAlabdullah Feb 25, 2025
45871c8
added test for component FormDesignNavigation
JamalAlabdullah Feb 25, 2025
1b39358
update
JamalAlabdullah Feb 25, 2025
17179ad
Merge branch 'main' into 14732-set-up-a-new-navigation-page
JamalAlabdullah Feb 25, 2025
1891db7
fixed coderabbitai warning
JamalAlabdullah Feb 25, 2025
c778934
Merge branch '14732-set-up-a-new-navigation-page' of https://github.c…
JamalAlabdullah Feb 25, 2025
4bee2ff
test
JamalAlabdullah Feb 26, 2025
3153a54
refacture
JamalAlabdullah Feb 26, 2025
76768c0
update
JamalAlabdullah Feb 26, 2025
ba2b0d9
update test
JamalAlabdullah Feb 26, 2025
3ed96e5
Merge branch 'main' into 14732-set-up-a-new-navigation-page
JamalAlabdullah Feb 26, 2025
243a7c2
added test
JamalAlabdullah Feb 26, 2025
fb490dd
Merge branch '14732-set-up-a-new-navigation-page' of https://github.c…
JamalAlabdullah Feb 26, 2025
1c11d90
added test
JamalAlabdullah Feb 26, 2025
f38acd5
added test
JamalAlabdullah Feb 26, 2025
ca1aa89
update test
JamalAlabdullah Feb 26, 2025
b2c67be
fixed css
JamalAlabdullah Feb 26, 2025
7a8f54e
Merge branch 'main' into 14732-set-up-a-new-navigation-page
JamalAlabdullah Feb 26, 2025
2c9c4ea
update test
JamalAlabdullah Feb 26, 2025
d38c8ca
Merge branch '14732-set-up-a-new-navigation-page' of https://github.c…
JamalAlabdullah Feb 26, 2025
ac40bbc
update size for app name
JamalAlabdullah Feb 27, 2025
5ab9fad
Merge branch 'main' into 14732-set-up-a-new-navigation-page
JamalAlabdullah Feb 27, 2025
a21e41d
updated size
JamalAlabdullah Feb 27, 2025
f09fae7
Merge branch '14732-set-up-a-new-navigation-page' of https://github.c…
JamalAlabdullah Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 72 additions & 4 deletions frontend/app-development/router/routes.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { routerRoutes } from './routes';
import { render, screen, waitFor } from '@testing-library/react';
import { routerRoutes, UiEditor } from './routes';
import { RoutePaths } from '../enums/RoutePaths';
import React from 'react';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
Expand All @@ -13,6 +13,8 @@ import { PreviewContextProvider } from '../contexts/PreviewContext';
import { AppDevelopmentContextProvider } from '../contexts/AppDevelopmentContext';
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';

// Mocks:
jest.mock('@altinn/ux-editor-v3/SubApp', () => ({
Expand All @@ -22,8 +24,67 @@ jest.mock('@altinn/ux-editor/SubApp', () => ({
SubApp: () => <div data-testid='latest version' />,
}));

jest.mock('@altinn/ux-editor/SubApp', () => ({
SubApp: ({ onLayoutSetNameChange }: { onLayoutSetNameChange: (name: string) => void }) => {
React.useEffect(() => {
onLayoutSetNameChange('test-layout');
}, [onLayoutSetNameChange]);
return <div data-testid='latest version' />;
},
}));

const renderWithProviders = (
ui: React.ReactElement,
queryClient: QueryClient,
layoutContextValue?: any,
) => {
return render(
<ServicesContextProvider {...queriesMock} client={queryClient}>
<SettingsModalContextProvider>
<PreviewContextProvider>
<AppDevelopmentContextProvider>
<LayoutContext.Provider value={layoutContextValue}>{ui}</LayoutContext.Provider>
</AppDevelopmentContextProvider>
</PreviewContextProvider>
</SettingsModalContextProvider>
</ServicesContextProvider>,
);
};

describe('routes', () => {
describe(RoutePaths.UIEditor, () => {
it('calls setSelectedLayoutSetName when onLayoutSetNameChange is triggered', () => {
const setSelectedLayoutSetName = jest.fn();
const queryClient = createQueryClientMock();
const appVersion: AppVersion = {
frontendVersion: '4.0.0',
backendVersion: '7.0.0',
};
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
const { rerender } = renderWithProviders(<UiEditor />, queryClient, {
setSelectedLayoutSetName,
});
const layoutSetName = 'test-layout';
const onLayoutSetNameChange = jest.fn();
rerender(
<UiEditorLatest
shouldReloadPreview={false}
previewHasLoaded={undefined}
onLayoutSetNameChange={onLayoutSetNameChange}
/>,
);
onLayoutSetNameChange(layoutSetName);
expect(setSelectedLayoutSetName).toHaveBeenCalledWith(layoutSetName);
});

it('Returns null when there is no AppVersion', async () => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppVersion, org, app], null);
renderUiEditor(queryClient);
expect(screen.queryByTestId('version 3')).not.toBeInTheDocument();
expect(screen.queryByTestId('latest version')).not.toBeInTheDocument();
});

type FrontendVersion = null | '3.0.0' | '4.0.0';
type PackageVersion = 'version 3' | 'latest version';
type TestCase = [PackageVersion, FrontendVersion];
Expand All @@ -36,18 +97,25 @@ describe('routes', () => {

it.each(testCases)(
'Renders the %s schema editor page when the app frontend version is %s',
(expectedPackage, frontendVersion) => {
async (expectedPackage, frontendVersion) => {
const appVersion: AppVersion = {
frontendVersion,
backendVersion: '7.0.0',
};
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
renderUiEditor(queryClient);
expect(screen.getByTestId(expectedPackage)).toBeInTheDocument();
expect(await screen.findByTestId(expectedPackage)).toBeInTheDocument();
},
);

it('renders a loading spinner', async () => {
renderUiEditor();
await waitFor(() => {
expect(screen.getByTestId('studio-spinner-test-id')).toBeInTheDocument();
});
});

it('renders a loading spinner while fetching frontend version', () => {
renderUiEditor();
expect(screen.getByText(textMock('ux_editor.loading_page'))).toBeInTheDocument();
Expand Down
41 changes: 30 additions & 11 deletions frontend/app-development/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import { useAppVersionQuery } from 'app-shared/hooks/queries';
import React from 'react';
import { usePreviewContext } from '../contexts/PreviewContext';
import { useLayoutContext } from '../contexts/LayoutContext';
import { StudioPageSpinner } from '@studio/components';
import { StudioPageSpinner, useLocalStorage } 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';
import { useAppConfigQuery } from 'app-development/hooks/queries';

interface IRouteProps {
headerTextKey?: string;
Expand All @@ -39,28 +42,44 @@ const latestFrontendVersion = '4';
const isLatestFrontendVersion = (version: AppVersion): boolean =>
version?.frontendVersion?.startsWith(latestFrontendVersion);

const UiEditor = () => {
export const UiEditor = () => {
const { org, app } = useStudioEnvironmentParams();
const { t } = useTranslation();
const { data: version, isPending: fetchingVersionIsPending } = useAppVersionQuery(org, app);
const { shouldReloadPreview, previewHasLoaded } = usePreviewContext();
const { setSelectedLayoutSetName } = useLayoutContext();
const [selectedFormLayoutSetName] = useLocalStorage<string>('layoutSet/' + app);
const isTaskNavigationEnabled = shouldDisplayFeature(FeatureFlag.TaskNavigation);

const { data: appConfigData } = useAppConfigQuery(org, app, {
hideDefaultError: true,
});
Comment on lines +54 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d suggest moving this code into FormDesignerNavigation, as it’s only used by FormDesignerNavigation and should not block the whole page from loading since it’s only used to show the appName.

Suggested change
const { data: appConfigData } = useAppConfigQuery(org, app, {
hideDefaultError: true,
});


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

if (!version) return null;

return isLatestFrontendVersion(version) ? (
<UiEditorLatest
shouldReloadPreview={shouldReloadPreview}
previewHasLoaded={previewHasLoaded}
onLayoutSetNameChange={(layoutSetName) => setSelectedLayoutSetName(layoutSetName)}
/>
) : (
<UiEditorV3 />
);
const renderUiEditorContent = () => {
if (isTaskNavigationEnabled && !selectedFormLayoutSetName && appConfigData) {
return <FormDesignerNavigation appConfig={appConfigData.serviceName} />;
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isTaskNavigationEnabled && !selectedFormLayoutSetName && appConfigData) {
return <FormDesignerNavigation appConfig={appConfigData.serviceName} />;
if (isTaskNavigationEnabled && !selectedFormLayoutSetName) {
return <FormDesignerNavigation />;

}

const handleLayoutSetNameChange = (layoutSetName: string) => {
setSelectedLayoutSetName(layoutSetName);
};

return (
<UiEditorLatest
shouldReloadPreview={shouldReloadPreview}
previewHasLoaded={previewHasLoaded}
onLayoutSetNameChange={handleLayoutSetNameChange}
/>
);
};

return isLatestFrontendVersion(version) ? renderUiEditorContent() : <UiEditorV3 />;
};

export const routerRoutes: RouterRoute[] = [
Expand Down
25 changes: 25 additions & 0 deletions frontend/packages/shared/src/utils/featureToggleUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ describe('featureToggle localStorage', () => {
it('should return false if feature is not enabled in the localStorage', () => {
expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy();
});

it('should return true if TaskNavigation is enabled in the localStorage', () => {
typedLocalStorage.setItem<string[]>('featureFlags', ['taskNavigation']);
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeTruthy();
});

it('should return false if TaskNavigation is not enabled in the localStorage', () => {
typedLocalStorage.setItem<string[]>('featureFlags', ['demo']);
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeFalsy();
});
});

describe('featureToggle url', () => {
Expand Down Expand Up @@ -69,6 +79,16 @@ describe('featureToggle url', () => {
]);
expect(typedLocalStorage.getItem<string[]>('featureFlags')).toBeNull();
});

it('should return true if TaskNavigation is enabled in the url', () => {
window.history.pushState({}, 'PageUrl', '/?featureFlags=taskNavigation');
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeTruthy();
});

it('should return false if TaskNavigation is not enabled in the url', () => {
window.history.pushState({}, 'PageUrl', '/?featureFlags=demo');
expect(shouldDisplayFeature(FeatureFlag.TaskNavigation)).toBeFalsy();
});
});

describe('addFeatureToLocalStorage', () => {
Expand All @@ -89,6 +109,11 @@ describe('addFeatureToLocalStorage', () => {
'shouldOverrideAppLibCheck',
]);
});

it('should add TaskNavigation to local storage', () => {
addFeatureFlagToLocalStorage(FeatureFlag.TaskNavigation);
expect(typedLocalStorage.getItem<string[]>('featureFlags')).toEqual(['taskNavigation']);
});
});

describe('removeFeatureFromLocalStorage', () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/utils/featureToggleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum FeatureFlag {
MainConfig = 'mainConfig',
OptionListEditor = 'optionListEditor',
ShouldOverrideAppLibCheck = 'shouldOverrideAppLibCheck',
TaskNavigation = 'taskNavigation',
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.wrapper {
background-color: #e6eff8;
}

.container {
margin: auto;
padding: var(--fds-spacing-6);
position: relative;
}

.header {
font-size: var(--fds-sizing-6);
}

.panel {
background-color: white;
border-radius: var(--fds-border_radius-medium);
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.05);
padding: var(--fds-spacing-10);
position: relative;
}

.content {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-10);
}

.footer {
border-top: 2px solid #efefef;
margin-top: var(--fds-spacing-10);
padding-top: var(--fds-spacing-8);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { FormDesignerNavigation, type FormDesignerNavigationProps } from './FormDesignerNavigation';
import { renderWithProviders } from 'app-development/test/mocks';
import { textMock } from '@studio/testing/mocks/i18nMock';

const defaultProps = {
appConfig: 'test',
};

describe('FormDesignerNavigation', () => {
it('renders the component with heading text test', () => {
render({ appConfig: 'test' });
expect(screen.getByText('test')).toBeInTheDocument();
});
it('renders the contact link', () => {
render();
expect(screen.getByRole('link', { name: textMock('general.contact') })).toBeInTheDocument();
});
});

const render = (props: Partial<FormDesignerNavigationProps> = {}) =>
renderWithProviders()(<FormDesignerNavigation {...defaultProps} {...props} />);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Link } from '@digdir/designsystemet-react';
import React from 'react';
import classes from './FormDesignerNavigation.module.css';
import { useTranslation } from 'react-i18next';

export type FormDesignerNavigationProps = {
appConfig: string;
};
Comment on lines +6 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type FormDesignerNavigationProps = {
appConfig: string;
};


export const FormDesignerNavigation = ({ appConfig }: FormDesignerNavigationProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const FormDesignerNavigation = ({ appConfig }: FormDesignerNavigationProps) => {
export const FormDesignerNavigation = () => {
const { org, app } = useStudioEnvironmentParams();
const { data: appConfigData } = useAppConfigQuery(org, app);

const { t } = useTranslation();
return (
<div className={classes.wrapper}>
<main className={classes.container}>
<div className={classes.panel}>
<div className={classes.content}>
<div className={classes.header}>{appConfig}</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div className={classes.header}>{appConfig}</div>
<div className={classes.header}>{appConfigData?.serviceName}</div>

</div>
<footer className={classes.footer}>
<Link href='/contact'>{t('general.contact')}</Link>
</footer>
</div>
</main>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FormDesignerNavigation } from './FormDesignerNavigation';