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

9360 receipt editor navigation with query params #9367

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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