Skip to content

Commit

Permalink
Fork ux-editor package
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasEng committed Feb 1, 2024
1 parent 540425e commit 5b46cb5
Show file tree
Hide file tree
Showing 437 changed files with 33,585 additions and 3 deletions.
61 changes: 61 additions & 0 deletions frontend/app-development/router/routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import { routerRoutes } from './routes';
import { RoutePaths } from '../enums/RoutePaths';
import React from 'react';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';
import { AppVersion } from 'app-shared/types/AppVersion';
import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import { queriesMock } from 'app-shared/mocks/queriesMock';

// Mocks:
jest.mock('../../packages/ux-editor-v3/src/SubApp', () => ({
SubApp: () => <div data-testid='version 3' />,
}));
jest.mock('../../packages/ux-editor/src/SubApp', () => ({
SubApp: () => <div data-testid='latest version' />,
}));

// Test data
const org = 'org';
const app = 'app';

describe('routes', () => {
describe(RoutePaths.UIEditor, () => {
type FrontendVersion = null | '3.0.0' | '4.0.0';
type PackageVersion = 'version 3' | 'latest version';
type TestCase = [PackageVersion, FrontendVersion];

const testCases: TestCase[] = [
['version 3', null],
['version 3', '3.0.0'],
['latest version', '4.0.0'],
];

it.each(testCases)(
'Renders the %s schema editor page when the app frontend version is %s',
(expectedPackage, frontendVersion) => {
renderUiEditor(frontendVersion);
expect(screen.getByTestId(expectedPackage)).toBeInTheDocument();
},
);

const renderUiEditor = (frontendVersion: string | null) =>
renderSubapp(RoutePaths.UIEditor, frontendVersion);
});
});

const renderSubapp = (path: RoutePathsm, frontendVersion: string = null) => {
const Subapp = routerRoutes.find((route) => route.path === path)!.subapp;
const appVersion: AppVersion = {
frontendVersion,
backendVersion: '7.0.0',
};
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
return render(
<ServicesContextProvider {...queriesMock} client={queryClient}>
<Subapp />
</ServicesContextProvider>,
);
};
19 changes: 17 additions & 2 deletions frontend/app-development/router/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { SubApp } from '../../packages/ux-editor/src/SubApp';
import { SubApp as UiEditorLatest } from '../../packages/ux-editor/src/SubApp';
import { SubApp as UiEditorV3 } from '../../packages/ux-editor-v3/src/SubApp';
import { Overview } from '../features/overview/components/Overview';
import { TextEditor } from '../features/textEditor/TextEditor';
import DataModellingContainer from '../features/dataModelling/containers/DataModellingContainer';
import { DeployPage } from '../features/appPublish/pages/deployPage';
import { ProcessEditor } from 'app-development/features/processEditor';
import { RoutePaths } from 'app-development/enums/RoutePaths';
import type { AppVersion } from 'app-shared/types/AppVersion';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { useAppVersionQuery } from 'app-shared/hooks/queries';
import React from 'react';

interface IRouteProps {
headerTextKey?: string;
Expand All @@ -25,10 +30,20 @@ interface RouterRoute {
props?: IRouteProps;
}

const latestFrontendVersion = '4';
const isLatestFrontendVersion = (version: AppVersion): boolean =>
version?.frontendVersion?.startsWith(latestFrontendVersion);

const UiEditor = () => {
const { org, app } = useStudioUrlParams();
const { data } = useAppVersionQuery(org, app);
return isLatestFrontendVersion(data) ? <UiEditorLatest /> : <UiEditorV3 />;
};

export const routerRoutes: RouterRoute[] = [
{
path: RoutePaths.UIEditor,
subapp: SubApp,
subapp: UiEditor,
},
{
path: RoutePaths.Overview,
Expand Down
8 changes: 7 additions & 1 deletion frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const config = {
transformIgnorePatterns: [
`node_modules(\\\\|/)(?!${packagesToTransform})`,
'\\.schema\\.v1\\.json$',
'nb.json$',
'en.json$',
],
reporters: ['default', 'jest-junit'],
moduleNameMapper: {
Expand All @@ -44,6 +46,7 @@ const config = {
'^@altinn/schema-editor/(.*)': path.join(__dirname, 'packages/schema-editor/src/$1'),
'^@altinn/schema-model/(.*)': path.join(__dirname, 'packages/schema-model/src/$1'),
'^@altinn/ux-editor/(.*)': path.join(__dirname, 'packages/ux-editor/src/$1'),
'^@altinn/ux-editor-v3/(.*)': path.join(__dirname, 'packages/ux-editor-v3/src/$1'),
'^@altinn/process-editor/(.*)': path.join(__dirname, 'packages/process-editor/src/$1'),
'^@altinn/policy-editor/(.*)': path.join(__dirname, 'packages/policy-editor/src/$1'),
'^@studio/icons': path.join(__dirname, 'libs/studio-icons/src/$1'),
Expand All @@ -60,6 +63,9 @@ if (process.env.CI) {
config.reporters.push('github-actions');
config.collectCoverage = true;
config.coverageReporters = ['lcov'];
config.coveragePathIgnorePatterns = ['frontend/packages/ux-editor/src/testing/'];
config.coveragePathIgnorePatterns = [
'frontend/packages/ux-editor/src/testing/',
'frontend/packages/ux-editor-v3/src/testing/',
];
}
module.exports = config;
1 change: 1 addition & 0 deletions frontend/packages/ux-editor-v3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tjeneste 3.0 react POC
1 change: 1 addition & 0 deletions frontend/packages/ux-editor-v3/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../jest.config');
36 changes: 36 additions & 0 deletions frontend/packages/ux-editor-v3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "ux-editor-v3",
"description": "",
"version": "1.0.1",
"author": "Altinn",
"dependencies": {
"@mui/material": "5.15.5",
"@reduxjs/toolkit": "1.9.7",
"@studio/icons": "workspace:^",
"axios": "1.6.5",
"classnames": "2.5.1",
"react": "18.2.0",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-modal": "3.16.1",
"react-redux": "8.1.3",
"react-select": "5.8.0",
"redux": "4.2.1",
"reselect": "4.1.8",
"typescript": "5.3.3",
"uuid": "9.0.1"
},
"devDependencies": {
"@redux-devtools/extension": "3.0.0",
"jest": "29.7.0"
},
"license": "3-Clause BSD",
"main": "index.js",
"peerDependencies": {
"webpack": "5.89.0"
},
"scripts": {
"test": "jest --maxWorkers=50%"
}
}
106 changes: 106 additions & 0 deletions frontend/packages/ux-editor-v3/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { formLayoutSettingsMock, renderWithProviders } from './testing/mocks';
import { App } from './App';
import { textMock } from '../../../testing/mocks/i18nMock';
import { typedLocalStorage } from 'app-shared/utils/webStorage';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { appStateMock } from './testing/stateMocks';
import type { AppContextProps } from './AppContext';
import ruleHandlerMock from './testing/ruleHandlerMock';
import { layoutSetsMock } from './testing/layoutMock';

const { selectedLayoutSet } = appStateMock.formDesigner.layout;

const renderApp = (
queries: Partial<ServicesContextProps> = {},
appContextProps: Partial<AppContextProps> = {},
) => {
return renderWithProviders(<App />, {
queries,
appContextProps,
});
};

describe('App', () => {
it('should render the spinner', () => {
renderApp({}, { selectedLayoutSet });
expect(screen.getByText(textMock('general.loading'))).toBeInTheDocument();
});

it('should render the component', async () => {
const mockQueries: Partial<ServicesContextProps> = {
getInstanceIdForPreview: jest.fn().mockImplementation(() => Promise.resolve('test')),
getRuleModel: jest.fn().mockImplementation(() => Promise.resolve(ruleHandlerMock)),
getLayoutSets: jest.fn().mockImplementation(() => Promise.resolve(layoutSetsMock)),
getFormLayoutSettings: jest
.fn()
.mockImplementation(() => Promise.resolve(formLayoutSettingsMock)),
};
renderApp(mockQueries, { selectedLayoutSet });
await waitFor(() =>
expect(screen.queryByText(textMock('general.loading'))).not.toBeInTheDocument(),
);
});

it('Removes the preview layout set from local storage if it does not exist', async () => {
const removeSelectedLayoutSetMock = jest.fn();
const layoutSetThatDoesNotExist = 'layout-set-that-does-not-exist';
const mockQueries: Partial<ServicesContextProps> = {
getInstanceIdForPreview: jest.fn().mockImplementation(() => Promise.resolve('test')),
getRuleModel: jest.fn().mockImplementation(() => Promise.resolve(ruleHandlerMock)),
getLayoutSets: jest.fn().mockImplementation(() => Promise.resolve(layoutSetsMock)),
getFormLayoutSettings: jest
.fn()
.mockImplementation(() => Promise.resolve(formLayoutSettingsMock)),
};
renderApp(mockQueries, {
selectedLayoutSet: layoutSetThatDoesNotExist,
removeSelectedLayoutSet: removeSelectedLayoutSetMock,
});
await waitFor(() =>
expect(screen.queryByText(textMock('general.loading'))).not.toBeInTheDocument(),
);
expect(removeSelectedLayoutSetMock).toHaveBeenCalledTimes(1);
});

it('Does not remove the preview layout set from local storage if it exists', async () => {
const removeSelectedLayoutSetMock = jest.fn();
const mockQueries: Partial<ServicesContextProps> = {
getInstanceIdForPreview: jest.fn().mockImplementation(() => Promise.resolve('test')),
getRuleModel: jest.fn().mockImplementation(() => Promise.resolve(ruleHandlerMock)),
getLayoutSets: jest.fn().mockImplementation(() => Promise.resolve(layoutSetsMock)),
getFormLayoutSettings: jest
.fn()
.mockImplementation(() => Promise.resolve(formLayoutSettingsMock)),
};
jest.spyOn(typedLocalStorage, 'getItem').mockReturnValue(selectedLayoutSet);
renderApp(mockQueries, {
selectedLayoutSet,
removeSelectedLayoutSet: removeSelectedLayoutSetMock,
});
await waitFor(() =>
expect(screen.queryByText(textMock('general.loading'))).not.toBeInTheDocument(),
);
expect(removeSelectedLayoutSetMock).not.toHaveBeenCalled();
});

it('Renders the unsupported version message if the version is not supported', async () => {
renderApp(
{
getAppVersion: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ backendVersion: '7.15.1', frontendVersion: '4.0.0-rc1' }),
),
},
{ selectedLayoutSet },
);

expect(
await screen.findByText(
textMock('ux_editor.unsupported_version_message_title', { version: 'V4' }),
),
).toBeInTheDocument();
});
});
104 changes: 104 additions & 0 deletions frontend/packages/ux-editor-v3/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { FormDesigner } from './containers/FormDesigner';
import { useText } from './hooks';
import { StudioPageSpinner } from '@studio/components';
import { ErrorPage } from './components/ErrorPage';
import { useDatamodelMetadataQuery } from './hooks/queries/useDatamodelMetadataQuery';
import { selectedLayoutNameSelector } from './selectors/formLayoutSelectors';
import { useWidgetsQuery } from './hooks/queries/useWidgetsQuery';
import { useTextResourcesQuery } from 'app-shared/hooks/queries/useTextResourcesQuery';
import { useLayoutSetsQuery } from './hooks/queries/useLayoutSetsQuery';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { useAppContext } from './hooks/useAppContext';
import { FormContextProvider } from './containers/FormContext';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
import { UnsupportedVersionMessage } from './components/UnsupportedVersionMessage';
import { useAppVersionQuery } from 'app-shared/hooks/queries/useAppVersionQuery';

/**
* This is the main React component responsible for controlling
* the mode of the application and loading initial data for the
* application
*/

export function App() {
const t = useText();
const { org, app } = useStudioUrlParams();
const selectedLayout = useSelector(selectedLayoutNameSelector);
const { selectedLayoutSet, setSelectedLayoutSet, removeSelectedLayoutSet } = useAppContext();
const { data: layoutSets, isSuccess: areLayoutSetsFetched } = useLayoutSetsQuery(org, app);
const { isSuccess: areWidgetsFetched, isError: widgetFetchedError } = useWidgetsQuery(org, app);
const { isSuccess: isDatamodelFetched, isError: dataModelFetchedError } =
useDatamodelMetadataQuery(org, app);
const { isSuccess: areTextResourcesFetched } = useTextResourcesQuery(org, app);
const { data: appVersion } = useAppVersionQuery(org, app);

useEffect(() => {
if (
areLayoutSetsFetched &&
selectedLayoutSet &&
(!layoutSets || !layoutSets.sets.map((set) => set.id).includes(selectedLayoutSet))
)
removeSelectedLayoutSet();
}, [
areLayoutSetsFetched,
layoutSets,
selectedLayoutSet,
setSelectedLayoutSet,
removeSelectedLayoutSet,
]);

const componentIsReady = areWidgetsFetched && isDatamodelFetched && areTextResourcesFetched;

const componentHasError = dataModelFetchedError || widgetFetchedError;

const mapErrorToDisplayError = (): { title: string; message: string } => {
const defaultTitle = t('general.fetch_error_title');
const defaultMessage = t('general.fetch_error_message');

const createErrorMessage = (resource: string): { title: string; message: string } => ({
title: `${defaultTitle} ${resource}`,
message: defaultMessage,
});

if (dataModelFetchedError) {
return createErrorMessage(t('general.dataModel'));
}
if (widgetFetchedError) {
return createErrorMessage(t('general.widget'));
}

return createErrorMessage(t('general.unknown_error'));
};

useEffect(() => {
if (selectedLayoutSet === null && layoutSets) {
// Only set layout set if layout sets exists and there is no layout set selected yet
setSelectedLayoutSet(layoutSets.sets[0].id);
}
}, [setSelectedLayoutSet, selectedLayoutSet, layoutSets, app]);

if (
appVersion?.frontendVersion?.startsWith('4') &&
!shouldDisplayFeature('shouldOverrideAppFrontendCheck')
) {
return (
<UnsupportedVersionMessage version='V4' closestSupportedVersion='V3' category='too-new' />
);
}

if (componentHasError) {
const mappedError = mapErrorToDisplayError();
return <ErrorPage title={mappedError.title} message={mappedError.message} />;
}

if (componentIsReady) {
return (
<FormContextProvider>
<FormDesigner selectedLayout={selectedLayout} selectedLayoutSet={selectedLayoutSet} />
</FormContextProvider>
);
}
return <StudioPageSpinner />;
}
11 changes: 11 additions & 0 deletions frontend/packages/ux-editor-v3/src/AppContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RefObject } from 'react';
import { createContext } from 'react';

export interface AppContextProps {
previewIframeRef: RefObject<HTMLIFrameElement>;
selectedLayoutSet: string;
setSelectedLayoutSet: (layoutSet: string) => void;
removeSelectedLayoutSet: () => void;
}

export const AppContext = createContext<AppContextProps>(null);
Loading

0 comments on commit 5b46cb5

Please sign in to comment.