Skip to content

Commit

Permalink
Move selected layout set to global ux-editor context
Browse files Browse the repository at this point in the history
  • Loading branch information
standeren committed Oct 19, 2023
1 parent 14b2c03 commit c40fc7a
Show file tree
Hide file tree
Showing 28 changed files with 105 additions and 146 deletions.
2 changes: 1 addition & 1 deletion backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ public IActionResult UpdateAttachmentWithTag(string org, string app, [FromQuery]
private string GetSelectedLayoutSetInEditorFromRefererHeader(string refererHeader)
{
Uri refererUri = new(refererHeader);
string layoutSetName = HttpUtility.ParseQueryString(refererUri.Query)["selectedLayoutSetInEditor"];
string layoutSetName = HttpUtility.ParseQueryString(refererUri.Query)["selectedLayoutSet"];

return string.IsNullOrEmpty(layoutSetName) ? null : layoutSetName;
}
Expand Down
10 changes: 4 additions & 6 deletions frontend/app-preview/src/views/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ export const LandingPage = ({ variant = 'preview' }: LandingPageProps) => {
const { data: instanceId } = useInstanceIdQuery(org, app);
const repoType = getRepositoryType(org, app);
const menu = getTopBarAppPreviewMenu(org, app, repoType, t);
const [selectedLayoutSetInEditor, setSelectedLayoutSetInEditor] = useLocalStorage<string>(
'layoutSet/' + app,
);
const [selectedLayoutSet, setSelectedLayoutSet] = useLocalStorage<string>('layoutSet/' + app, null);
const [previewViewSize, setPreviewViewSize] = useLocalStorage<PreviewAsViewSize>(
'viewSize',
'desktop',
Expand All @@ -43,7 +41,7 @@ export const LandingPage = ({ variant = 'preview' }: LandingPageProps) => {
input !== null && input.tagName === 'IFRAME';

const handleChangeLayoutSet = (layoutSet: string) => {
setSelectedLayoutSetInEditor(layoutSet);
setSelectedLayoutSet(layoutSet);
// might need to remove selected layout from local storage to make sure first page is selected
window.location.reload();
};
Expand Down Expand Up @@ -79,7 +77,7 @@ export const LandingPage = ({ variant = 'preview' }: LandingPageProps) => {
<AppPreviewSubMenu
setViewSize={setPreviewViewSize}
viewSize={previewViewSize}
selectedLayoutSet={selectedLayoutSetInEditor}
selectedLayoutSet={selectedLayoutSet}
handleChangeLayoutSet={handleChangeLayoutSet}
/>
}
Expand All @@ -89,7 +87,7 @@ export const LandingPage = ({ variant = 'preview' }: LandingPageProps) => {
<iframe
title={t('preview.iframe_title')}
id='app-frontend-react-iframe'
src={previewPage(org, app, selectedLayoutSetInEditor)}
src={previewPage(org, app, selectedLayoutSet)}
className={previewViewSize === 'desktop' ? classes.iframeDesktop : classes.iframeMobile}
/>
{previewViewSize === 'mobile' && <div className={classes.iframeMobileViewOverlay}></div>}
Expand Down
20 changes: 17 additions & 3 deletions frontend/packages/shared/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { TypedStorage, typedLocalStorage } from 'app-shared/utils/webStorage';

const useWebStorage = <T>(
Expand All @@ -8,6 +8,20 @@ const useWebStorage = <T>(
): [T, (newValue: T) => void, () => void] => {
const [value, setValue] = useState<T>(typedStorage.getItem(key) || initialValue);

const handleStorageChange = useCallback(() => {
const item = typedStorage.getItem<T>(key);
setValue(item);
}, [key, setValue]);

Check warning on line 14 in frontend/packages/shared/src/hooks/useLocalStorage.ts

View workflow job for this annotation

GitHub Actions / Typechecking and linting

React Hook useCallback has a missing dependency: 'typedStorage'. Either include it or remove the dependency array

useEffect(() => {

window.addEventListener('storage', handleStorageChange);

return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [handleStorageChange]);

const setStorageValue = useCallback(
(newValue: T) => {
typedStorage.setItem(key, newValue);
Expand All @@ -31,5 +45,5 @@ const useWebStorage = <T>(
* @description
* useLocalStorage is a hook that allows you to use local storage the same way you would with useState
*/
export const useLocalStorage = <T, K = string>(key: K, initialValue?: T) =>
useWebStorage<T>(typedLocalStorage, key as string, initialValue);
export const useLocalStorage = <T>(key: string, initialValue?: T) =>
useWebStorage<T>(typedLocalStorage, key, initialValue);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { previewSignalRHubSubPath } from 'app-shared/api/paths';
import { HubConnection, HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

const PreviewConnectionContext = createContext<HubConnection>(null);

Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/utils/webStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type TypedStorage = {
removeItem: (key: string) => void;
};

type WebStorage = Pick<Storage, 'setItem' | 'getItem' | 'removeItem'>;
type WebStorage = Pick<Storage, 'setItem' | 'getItem' | 'removeItem' >;

const createWebStorage = (storage: WebStorage): TypedStorage => {
if (!storage) {
Expand Down
36 changes: 11 additions & 25 deletions frontend/packages/ux-editor/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { FormDesigner } from './containers/FormDesigner';
import { FormLayoutActions } from './features/formDesigner/formLayout/formLayoutSlice';
import { useText } from './hooks';
import { PageSpinner } from 'app-shared/components/PageSpinner';
import { ErrorPage } from './components/ErrorPage';
import { useDatamodelMetadataQuery } from './hooks/queries/useDatamodelMetadataQuery';
import {
selectedLayoutNameSelector,
selectedLayoutSetSelector,
} from './selectors/formLayoutSelectors';
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';
Expand All @@ -29,11 +25,10 @@ export function App() {
const { org, app } = useStudioUrlParams();
const selectedLayout = useSelector(selectedLayoutNameSelector);
const [
selectedLayoutSetInPreview,
setSelectedLayoutSetInPreview,
removeSelectedLayoutSetInPreview,
selectedLayoutSet,
setSelectedLayoutSet,
removeSelectedLayoutSet,
] = useLocalStorage('layoutSet/' + app, null);
const selectedLayoutSet = useSelector(selectedLayoutSetSelector);
const { data: layoutSets, isSuccess: areLayoutSetsFetched } = useLayoutSetsQuery(org, app);
const { isSuccess: areWidgetsFetched, isError: widgetFetchedError } = useWidgetsQuery(org, app);
const { isSuccess: isDatamodelFetched, isError: dataModelFetchedError } =
Expand All @@ -43,16 +38,16 @@ export function App() {
useEffect(() => {
if (
areLayoutSetsFetched &&
selectedLayoutSetInPreview &&
(!layoutSets || !layoutSets.sets.map((set) => set.id).includes(selectedLayoutSetInPreview))
selectedLayoutSet &&
(!layoutSets || !layoutSets.sets.map((set) => set.id).includes(selectedLayoutSet))
)
removeSelectedLayoutSetInPreview();
removeSelectedLayoutSet();
}, [
areLayoutSetsFetched,
layoutSets,
selectedLayoutSetInPreview,
setSelectedLayoutSetInPreview,
removeSelectedLayoutSetInPreview,
selectedLayoutSet,
setSelectedLayoutSet,
removeSelectedLayoutSet,
]);

const componentIsReady = areWidgetsFetched && isDatamodelFetched && areTextResourcesFetched;
Expand Down Expand Up @@ -81,19 +76,10 @@ export function App() {
useEffect(() => {
if (selectedLayoutSet === null && layoutSets) {
// Only set layout set if layout sets exists and there is no layout set selected yet
dispatch(FormLayoutActions.updateSelectedLayoutSet(layoutSets.sets[0].id));
typedLocalStorage.setItem<string>('layoutSet/' + app, layoutSets.sets[0].id);
}
}, [dispatch, selectedLayoutSet, layoutSets, app]);

useEffect(() => {
const layoutSetInEditor = selectedLayoutSetInPreview ?? selectedLayoutSet;
if (layoutSets && layoutSetInEditor !== null && layoutSetInEditor !== '') {
typedLocalStorage.setItem<string>('layoutSet/' + app, layoutSetInEditor);
dispatch(FormLayoutActions.updateSelectedLayoutSet(layoutSetInEditor));
}
}, [dispatch, selectedLayoutSet, layoutSets, selectedLayoutSetInPreview, app]);

if (componentHasError) {
const mappedError = mapErrorToDisplayError();
return <ErrorPage title={mappedError.title} message={mappedError.message} />;
Expand All @@ -103,7 +89,7 @@ export function App() {
return (
<FormDesigner
selectedLayout={selectedLayout}
selectedLayoutSet={selectedLayoutSetInPreview ?? selectedLayoutSet}
selectedLayoutSet={selectedLayoutSet}
/>
);
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/packages/ux-editor/src/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { createContext, RefObject } from 'react';

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

export const AppContext = createContext<AppContextProps>({
previewIframeRef: null,
selectedLayoutSet: undefined,
setSelectedLayoutSet: (layoutSet: string) => {},
});
7 changes: 6 additions & 1 deletion frontend/packages/ux-editor/src/SubApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import { App } from './App';
import { store } from './store';
import './styles/index.css';
import { AppContext } from './AppContext';
import { useLocalStorage } from 'app-shared/hooks/useLocalStorage';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';

export const SubApp = () => {
const previewIframeRef = useRef<HTMLIFrameElement>(null);
const { app } = useStudioUrlParams();
const [selectedLayoutSet, setSelectedLayoutSet] = useLocalStorage('layoutSet/' + app, null);

return (
<Provider store={store}>
<AppContext.Provider value={{ previewIframeRef }}>
<AppContext.Provider value={{ previewIframeRef, selectedLayoutSet, setSelectedLayoutSet }}>
<App />
</AppContext.Provider>
</Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { ConfPageToolbar } from './ConfPageToolbar';
import { DefaultToolbar } from './DefaultToolbar';
import { _useIsProdHack } from 'app-shared/utils/_useIsProdHack';
import { Heading, Paragraph } from '@digdir/design-system-react';
import { useText } from '../../hooks';
import {
selectedLayoutNameSelector,
selectedLayoutSetSelector,
} from '../../selectors/formLayoutSelectors';
import { useFormLayoutSettingsQuery } from '../../hooks/queries/useFormLayoutSettingsQuery';
import { useLayoutSetsQuery } from '../../hooks/queries/useLayoutSetsQuery';
Expand All @@ -17,11 +15,12 @@ import { Accordion } from '@digdir/design-system-react';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
import classes from './Elements.module.css';
import { useAppContext } from '../../hooks/useAppContext';

export const Elements = () => {
const { org, app } = useStudioUrlParams();
const selectedLayout: string = useSelector(selectedLayoutNameSelector);
const selectedLayoutSet: string = useSelector(selectedLayoutSetSelector);
const { selectedLayoutSet } = useAppContext();
const layoutSetsQuery = useLayoutSetsQuery(org, app);
const { data: formLayoutSettings } = useFormLayoutSettingsQuery(org, app, selectedLayoutSet);
const receiptName = formLayoutSettings?.receiptLayoutName;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { LayoutSetsContainer } from './LayoutSetsContainer';
import userEvent from '@testing-library/user-event';
import { renderWithMockStore } from '../../testing/mocks';
import { layoutSetsMock } from '../../testing/layoutMock';
import { useDispatch } from 'react-redux';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
Expand All @@ -27,25 +25,7 @@ describe('LayoutSetsContainer', () => {
render();
expect(screen.getByRole('combobox')).toBeInTheDocument();
});

it('calls dispatch when selecting an option', async () => {
const dispatch = jest.fn();
(useDispatch as jest.Mock).mockReturnValue(dispatch);
render();

await waitFor(async () => {
await userEvent.selectOptions(
screen.getByRole('combobox'),
screen.getByRole('option', { name: layoutSetsMock.sets[0].id }),
);
});

expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith({
payload: layoutSetsMock.sets[0].id,
type: 'formDesigner/updateSelectedLayoutSet',
});
});

});

const render = () => renderWithMockStore()(<LayoutSetsContainer />);
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLayoutSetsQuery } from '../../hooks/queries/useLayoutSetsQuery';
import { selectedLayoutSetSelector } from '../../selectors/formLayoutSelectors';
import { FormLayoutActions } from '../../features/formDesigner/formLayout/formLayoutSlice';
import { NativeSelect } from '@digdir/design-system-react';
import { typedLocalStorage } from 'app-shared/utils/webStorage';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { useText } from '../../hooks';
import classes from './LayoutSetsContainer.module.css';
import { useAppContext } from '../../hooks/useAppContext';

export function LayoutSetsContainer() {
const { org, app } = useStudioUrlParams();
const dispatch = useDispatch();
const layoutSetsQuery = useLayoutSetsQuery(org, app);
const layoutSetNames = layoutSetsQuery.data?.sets?.map((set) => set.id);
const selectedLayoutSet: string = useSelector(selectedLayoutSetSelector);
const t = useText();
const { selectedLayoutSet, setSelectedLayoutSet } = useAppContext();

const onLayoutSetClick = (set: string) => {
dispatch(FormLayoutActions.updateSelectedLayoutSet(set));
typedLocalStorage.setItem<string>('layoutSet/' + app, set);
if (selectedLayoutSet !== set){
setSelectedLayoutSet(set);
}
};

if (!layoutSetNames) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import { ITextResource } from 'app-shared/types/global';
import { TrashIcon } from '@navikt/aksel-icons';
import { formItemConfigs } from '../../data/formItemConfig';
import { getComponentTitleByComponentType, getTextResource, truncate } from '../../utils/language';
import { selectedLayoutSetSelector } from '../../selectors/formLayoutSelectors';
import { textResourcesByLanguageSelector } from '../../selectors/textResourceSelectors';
import { useDeleteFormComponentMutation } from '../../hooks/mutations/useDeleteFormComponentMutation';
import { useTextResourcesSelector } from '../../hooks';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AltinnConfirmDialog } from 'app-shared/components';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { useAppContext } from '../../hooks/useAppContext';

export interface IFormComponentProps {
component: IFormComponent;
Expand All @@ -37,7 +36,6 @@ export const FormComponent = memo(function FormComponent({
handleDiscard,
handleEdit,
handleSave,
debounceSave,
id,
isEditMode,
}: IFormComponentProps) {
Expand All @@ -47,14 +45,14 @@ export const FormComponent = memo(function FormComponent({
const textResources: ITextResource[] = useTextResourcesSelector<ITextResource[]>(
textResourcesByLanguageSelector(DEFAULT_LANGUAGE),
);
const selectedLayoutSetName = useSelector(selectedLayoutSetSelector);
const { selectedLayoutSet } = useAppContext();
const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = useState<boolean>();
const Icon = formItemConfigs[component.type]?.icon;

const { mutate: deleteFormComponent } = useDeleteFormComponentMutation(
org,
app,
selectedLayoutSetName,
selectedLayoutSet,
);

const handleDelete = (): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import classes from './Preview.module.css';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { useSelector } from 'react-redux';
import cn from 'classnames';
import {
selectedLayoutNameSelector,
selectedLayoutSetSelector,
} from '../../selectors/formLayoutSelectors';
import { selectedLayoutNameSelector } from '../../selectors/formLayoutSelectors';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../../hooks/useAppContext';
import { useUpdate } from 'app-shared/hooks/useUpdate';
Expand Down Expand Up @@ -40,7 +37,7 @@ const NoSelectedPageMessage = () => {
const PreviewFrame = () => {
const { org, app } = useStudioUrlParams();
const [viewportToSimulate, setViewportToSimulate] = useState<SupportedView>('desktop');
const selectedLayoutSet = useSelector(selectedLayoutSetSelector);
const { selectedLayoutSet } = useAppContext();
const { t } = useTranslation();
const { previewIframeRef } = useAppContext();
const layoutName = useSelector(selectedLayoutNameSelector);
Expand Down
Loading

0 comments on commit c40fc7a

Please sign in to comment.