Skip to content

Commit

Permalink
9360 receipt editor navigation with query params (#9367)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael authored and Andrea Standeren committed Dec 15, 2022
1 parent 7e42f9c commit b14dfc0
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export const isNumeric = (str: string) => parseInt(str).toString() === str;
export const deepCopy = (value: any) => JSON.parse(JSON.stringify(value));
export const removeKey = (obj: any, key: string) => {
const copy = deepCopy(obj);
delete copy[key];
return copy;
};
30 changes: 26 additions & 4 deletions src/studio/src/designer/frontend/packages/ux-editor/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useEffect } from 'react';
import postMessages from 'app-shared/utils/postMessages';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { ErrorMessageComponent } from './components/message/ErrorMessageComponent';
import FormDesigner from './containers/FormDesigner';
import { FormDesigner } from './containers/FormDesigner';
import { FormLayoutActions } from './features/formDesigner/formLayout/formLayoutSlice';
import { loadTextResources } from './features/appData/textResources/textResourcesSlice';
import { fetchWidgets, fetchWidgetSettings } from './features/widgets/widgetsSlice';
import { fetchDataModel } from './features/appData/dataModel/dataModelSlice';
import { fetchLanguage } from './features/appData/language/languageSlice';
import { fetchRuleModel } from './features/appData/ruleModel/ruleModelSlice';
import { fetchServiceConfiguration } from './features/serviceConfigurations/serviceConfigurationSlice';
import { useParams } from 'react-router-dom';
import { useParams, useSearchParams } from 'react-router-dom';
import { textResourcesPath } from 'app-shared/api-paths';
import type { IAppState } from './types/global';
import { deepCopy } from 'app-shared/pure';

/**
* This is the main React component responsible for controlling
Expand All @@ -22,11 +24,31 @@ import { textResourcesPath } from 'app-shared/api-paths';
export function App() {
const dispatch = useDispatch();
const { org, app } = useParams();
const languageCode = 'nb';
const [searchParams, setSearchParams] = useSearchParams();
const layoutOrder = useSelector(
(state: IAppState) => state.formDesigner.layout.layoutSettings.pages.order
);
const selectedLayout = useSelector(
(state: IAppState) => state.formDesigner.layout.selectedLayout
);

// Set Layout to first layout in the page set if none is selected.
useEffect(() => {
if (!searchParams.has('layout') && layoutOrder[0]) {
setSearchParams({ ...deepCopy(searchParams), layout: layoutOrder[0] });
}
if (selectedLayout === 'default' && searchParams.has('layout')) {
dispatch(
FormLayoutActions.updateSelectedLayout({ selectedLayout: searchParams.get('layout') })
);
}
}, [dispatch, layoutOrder, searchParams, setSearchParams, selectedLayout]);

useEffect(() => {
const fetchFiles = () => {
dispatch(fetchDataModel());
dispatch(FormLayoutActions.fetchFormLayout());
const languageCode = 'nb';
dispatch(loadTextResources({ url: textResourcesPath(org, app, languageCode) }));
dispatch(fetchServiceConfiguration());
dispatch(fetchRuleModel());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { IAppState, LogicMode } from '../../types/global';
import classes from './RightMenu.module.css';
import { Add } from '@navikt/ds-icons';
import { Button, ButtonVariant } from '@altinn/altinn-design-system';
import { deepCopy } from 'app-shared/pure';
import { useSearchParams } from 'react-router-dom';

export interface IRightMenuProps {
toggleFileEditor: (mode?: LogicMode) => void;
Expand All @@ -17,6 +19,7 @@ export interface IRightMenuProps {

export default function RightMenu(props: IRightMenuProps) {
const [conditionalModalOpen, setConditionalModalOpen] = React.useState<boolean>(false);
const [searchParams, setSearchParams] = useSearchParams();
const [ruleModalOpen, setRuleModalOpen] = React.useState<boolean>(false);
const layoutOrder = useSelector(
(state: IAppState) => state.formDesigner.layout.layoutSettings.pages.order
Expand All @@ -34,6 +37,7 @@ export default function RightMenu(props: IRightMenuProps) {
function handleAddPage() {
const name = t('right_menu.page') + (layoutOrder.length + 1);
dispatch(FormLayoutActions.addLayout({ layout: name }));
setSearchParams({ ...deepCopy(searchParams), layout: name });
}

return (
Expand All @@ -53,8 +57,7 @@ export default function RightMenu(props: IRightMenuProps) {
</div>
<div className={classes.headerSection}>{t('right_menu.dynamics')}</div>
<div className={classes.contentSection}>
{t('right_menu.dynamics_description')}
&nbsp;
{t('right_menu.dynamics_description')}&nbsp;
<a
target='_blank'
rel='noopener noreferrer'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import type { ChangeEvent, SyntheticEvent, MouseEvent } from 'react';
import React, { useEffect, useState } from 'react';
import { Button, ButtonVariant, TextField } from '@altinn/altinn-design-system';
import { getLanguageFromKey, getParsedLanguageFromKey } from 'app-shared/utils/language';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -10,6 +11,8 @@ import { EllipsisV, Right } from '@navikt/ds-icons';
import classes from './PageElement.module.css';
import { Divider } from 'app-shared/primitives';
import cn from 'classnames';
import { useSearchParams } from 'react-router-dom';
import { deepCopy, removeKey } from 'app-shared/pure';

export interface IPageElementProps {
name: string;
Expand All @@ -18,47 +21,42 @@ export interface IPageElementProps {

export function PageElement({ name, invalid }: IPageElementProps) {
const dispatch = useDispatch();

const [searchParams, setSearchParams] = useSearchParams();
const selectedLayout = searchParams.get('layout');
const language = useSelector((state: IAppState) => state.appData.languageState.language);
const t = (key: string) => getLanguageFromKey(key, language);
const selectedLayout = useSelector(
(state: IAppState) => state.formDesigner.layout.selectedLayout
);
const layoutOrder = useSelector(
(state: IAppState) => state.formDesigner.layout.layoutSettings.pages.order
);
const [editMode, setEditMode] = React.useState<boolean>(false);
const [errorMessage, setErrorMessage] = React.useState<string>('');
const [newName, setNewName] = React.useState<string>('');
const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(null);
const [deleteAnchorEl, setDeleteAnchorEl] = React.useState<null | Element>(null);
const [editMode, setEditMode] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const [newName, setNewName] = useState<string>('');
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [deleteAnchorEl, setDeleteAnchorEl] = useState<null | Element>(null);
const disableUp = layoutOrder.indexOf(name) === 0;
const disableDown = layoutOrder.indexOf(name) === layoutOrder.length - 1;

useEffect(() => {
if (name !== selectedLayout) {
setEditMode(false);
}
}, [name, selectedLayout]);

const onPageClick = () => {
if (invalid) {
alert(`${name}: ${t('right_menu.pages.invalid_page_data')}`);
}
else if (selectedLayout !== name) {
} else if (selectedLayout !== name) {
dispatch(FormLayoutActions.updateSelectedLayout({ selectedLayout: name }));
setSearchParams({ ...deepCopy(searchParams), layout: name });
}
};

const onPageSettingsClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
const onPageSettingsClick = (event: MouseEvent<HTMLButtonElement>) =>
setMenuAnchorEl(event.currentTarget);
};

const onMenuClose = (event: React.SyntheticEvent) => {
event.stopPropagation();
setMenuAnchorEl(null);
};
const onMenuClose = (_event: SyntheticEvent) => setMenuAnchorEl(null);

const onMenuItemClick = (
event: React.SyntheticEvent,
action: 'up' | 'down' | 'edit' | 'delete'
) => {
event.stopPropagation();
const onMenuItemClick = (event: SyntheticEvent, action: 'up' | 'down' | 'edit' | 'delete') => {
if (action === 'delete') {
setDeleteAnchorEl(event.currentTarget);
} else if (action === 'edit') {
Expand All @@ -75,21 +73,18 @@ export function PageElement({ name, invalid }: IPageElementProps) {
setMenuAnchorEl(null);
};

const handleOnBlur = (event: any) => {
event.stopPropagation();
const handleOnBlur = async (_event: any) => {
setEditMode(false);
if (!errorMessage && name !== newName) {
dispatch(FormLayoutActions.updateLayoutName({ oldName: name, newName }));
await dispatch(FormLayoutActions.updateLayoutName({ oldName: name, newName }));
setSearchParams({ ...deepCopy(searchParams), layout: newName });
}
};

const pageNameExists = (candidateName: string): boolean => {
return layoutOrder.some(
(pageName: string) => pageName.toLowerCase() === candidateName.toLowerCase()
);
};
const pageNameExists = (candidateName: string): boolean =>
layoutOrder.some((p: string) => p.toLowerCase() === candidateName.toLowerCase());

const handleOnChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const nameRegex = new RegExp('^[a-zA-Z0-9_\\-\\.]*$');
const newNameCandidate = event.target.value.replace(/[/\\?%*:|"<>]/g, '-').trim();
if (pageNameExists(newNameCandidate)) {
Expand All @@ -106,30 +101,29 @@ export function PageElement({ name, invalid }: IPageElementProps) {
}
};

const handleKeyPress = (event: any) => {
event.stopPropagation();
const handleKeyPress = async (event: any) => {
if (event.key === 'Enter' && !errorMessage && name !== newName) {
dispatch(FormLayoutActions.updateLayoutName({ oldName: name, newName }));
await dispatch(FormLayoutActions.updateLayoutName({ oldName: name, newName }));
setSearchParams({ ...deepCopy(searchParams), layout: newName });
setEditMode(false);
} else if (event.key === 'Escape') {
setEditMode(false);
setNewName('');
}
};

const handleConfirmDeleteClose = (event?: React.SyntheticEvent) => {
event?.stopPropagation();
setDeleteAnchorEl(null);
};
const handleConfirmDeleteClose = () => setDeleteAnchorEl(null);

const handleConfirmDelete = (event?: React.SyntheticEvent) => {
event?.stopPropagation();
const handleConfirmDelete = () => {
setDeleteAnchorEl(null);
dispatch(FormLayoutActions.deleteLayout({ layout: name }));
setSearchParams(removeKey(searchParams, 'layout'));
};

return (
<div className={cn({ [classes.selected]: selectedLayout === name, [classes.invalid]: invalid })}>
<div
className={cn({ [classes.selected]: selectedLayout === name, [classes.invalid]: invalid })}
>
<div className={classes.elementContainer}>
<div>
<Right
Expand All @@ -140,30 +134,27 @@ export function PageElement({ name, invalid }: IPageElementProps) {
}}
/>
</div>
<div onClick={onPageClick}>
{!editMode && name}
{editMode && (
<>
<TextField
onBlur={handleOnBlur}
onKeyDown={handleKeyPress}
onChange={handleOnChange}
defaultValue={name}
isValid={!errorMessage}
/>
<span className={classes.errorMessage}>{errorMessage}</span>
</>
)}
</div>
<div>
<Button
className={classes.ellipsisButton}
icon={<EllipsisV />}
onClick={onPageSettingsClick}
style={menuAnchorEl ? { visibility: 'visible' } : {}}
variant={ButtonVariant.Quiet}
/>
</div>
{editMode ? (
<>
<TextField
onBlur={handleOnBlur}
onKeyDown={handleKeyPress}
onChange={handleOnChange}
defaultValue={name}
isValid={!errorMessage}
/>
<span className={classes.errorMessage}>{errorMessage}</span>
</>
) : (
<div onClick={onPageClick}>{name}</div>
)}
<Button
className={classes.ellipsisButton}
icon={<EllipsisV />}
onClick={onPageSettingsClick}
style={menuAnchorEl ? { visibility: 'visible' } : {}}
variant={ButtonVariant.Quiet}
/>
</div>

<AltinnMenu anchorEl={menuAnchorEl} open={Boolean(menuAnchorEl)} onClose={onMenuClose}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export class ContainerComponent extends Component<IContainerProps, IContainerSta
<div className={classes.formGroupBar}>
<Button
color={ButtonColor.Secondary}
icon={expanded ? <Collapse/> : <Expand/>}
icon={expanded ? <Collapse /> : <Expand />}
onClick={this.handleExpand}
variant={ButtonVariant.Quiet}
/>
Expand Down Expand Up @@ -500,18 +500,18 @@ export class ContainerComponent extends Component<IContainerProps, IContainerSta
public renderHoverIcons = (): JSX.Element => (
<>
<Button
icon={<Delete/>}
icon={<Delete />}
onClick={this.handleContainerDelete}
variant={ButtonVariant.Quiet}
/>
<Button icon={<Edit/>} onClick={this.handleEditMode} variant={ButtonVariant.Quiet} />
<Button icon={<Edit />} onClick={this.handleEditMode} variant={ButtonVariant.Quiet} />
</>
);

public renderEditIcons = (): JSX.Element => (
<>
<Button icon={<Error/>} onClick={this.handleDiscard} variant={ButtonVariant.Quiet} />
<Button icon={<Success/>} onClick={this.handleDiscard} variant={ButtonVariant.Quiet} />
<Button icon={<Error />} onClick={this.handleDiscard} variant={ButtonVariant.Quiet} />
<Button icon={<Success />} onClick={this.handleDiscard} variant={ButtonVariant.Quiet} />
</>
);

Expand Down Expand Up @@ -600,16 +600,11 @@ const makeMapStateToProps = () => {
return {
...props,
activeList: state.formDesigner.layout.activeList,
isBaseContainer: props.isBaseContainer,
components: GetLayoutComponentsSelector(state),
containers: GetLayoutContainersSelector(state),
dataModel: state.appData.dataModel.model,
dataModelGroup: container?.dataModelGroup,
dispatch: props.dispatch,
dndEvents: props.dndEvents,
formContainerActive: GetActiveFormContainer(state, props),
id: props.id,
index: props.index,
itemOrder: !props.items ? itemOrder : props.items,
language: state.appData.languageState.language,
repeating: container?.repeating,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const DesignView = (initialState: IDesignerPreviewState) => {
};
const baseContainerId =
Object.keys(state.layoutOrder).length > 0 ? Object.keys(state.layoutOrder)[0] : null;

const dndEvents: EditorDndEvents = {
moveItem,
moveItemToBottom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { fetchServiceConfiguration } from '../features/serviceConfigurations/ser
import { FormLayoutActions } from '../features/formDesigner/formLayout/formLayoutSlice';
import type { IAppState, IDataModelFieldElement, LogicMode } from '../types/global';
import { makeGetLayoutOrderSelector } from '../selectors/getLayoutData';
import { deepCopy } from 'app-shared/pure';

const useTheme = createTheme(altinnTheme);

Expand Down Expand Up @@ -110,7 +111,7 @@ const useStyles = makeStyles((theme: Theme) => ({
},
}));

function FormDesigner() {
export function FormDesigner() {
const classes = useStyles(useTheme);
const dispatch = useDispatch();

Expand Down Expand Up @@ -163,14 +164,12 @@ function FormDesigner() {

const activeList = useSelector((state: IAppState) => state.formDesigner.layout.activeList);
const layoutOrder = useSelector((state: IAppState) =>
JSON.parse(
JSON.stringify(
state.formDesigner.layout.layouts[state.formDesigner.layout.selectedLayout]?.order || {}
)
deepCopy(
state.formDesigner.layout.layouts[state.formDesigner.layout.selectedLayout]?.order || {}
)
);

const order = useSelector((state: IAppState) => makeGetLayoutOrderSelector()(state));
const order = useSelector(makeGetLayoutOrderSelector());

return (
<DndProvider backend={HTML5Backend}>
Expand Down Expand Up @@ -225,5 +224,3 @@ function FormDesigner() {
</DndProvider>
);
}

export default FormDesigner;
Loading

0 comments on commit b14dfc0

Please sign in to comment.