diff --git a/src/components/Icons/IconPlus.tsx b/src/components/Icons/IconPlus.tsx
new file mode 100644
index 0000000..3a078a7
--- /dev/null
+++ b/src/components/Icons/IconPlus.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+
+interface IconPlusProps {
+ fill?: string;
+ width?: string;
+ height?: string;
+ className?: string;
+}
+
+export const IconPlus = ({
+ fill = '#000',
+ width = '20',
+ height = '20',
+ className = '',
+}: IconPlusProps) => (
+
+);
diff --git a/src/components/Menu/Items/NodeTreeView.tsx b/src/components/Menu/Items/NodeTreeView.tsx
index 5d00344..e9ce5c3 100644
--- a/src/components/Menu/Items/NodeTreeView.tsx
+++ b/src/components/Menu/Items/NodeTreeView.tsx
@@ -1,29 +1,286 @@
import React from 'react';
-import { Box } from '@mui/material';
+import {
+ Box,
+ Divider,
+ IconButton,
+ ListItemText,
+ Menu,
+ MenuItem,
+ MenuList,
+ Typography,
+} from '@mui/material';
import { TreeItem, TreeView } from '@mui/lab';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import ExpandLessIcon from '@mui/icons-material/ExpandLess';
-import { EnumInputAction, IEditingNode, INode } from '../../../types';
+import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
+import MoreVertIcon from '@mui/icons-material/MoreVert';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import { EnumInputAction, IEditingNode, INode, MenuMetaType } from '../../../types';
+import { EnumInputActionScreen } from '../../../pages/Menu/Items';
+import { IconPlus } from '../../Icons/IconPlus';
+
+interface CustomTreeItemProps {
+ node: INode;
+ color: string;
+ fontWeight: string;
+ renderNodes: (nodes: INode[]) => JSX.Element[];
+ setSelected: (selected: string) => void;
+ emptyEditingNode: IEditingNode;
+ editingNode: IEditingNode;
+ setEditingNode: (editingNode: IEditingNode) => void;
+ handleUpdate: () => Promise;
+ data: any;
+ setOperationScreen: (operationScreen: EnumInputActionScreen) => void;
+ insertingNodeRef?: (node: HTMLElement) => void;
+}
+
+const CustomTreeItem = ({
+ node,
+ color,
+ fontWeight,
+ renderNodes,
+ setSelected,
+ emptyEditingNode,
+ editingNode,
+ setEditingNode,
+ handleUpdate,
+ data,
+ setOperationScreen,
+ insertingNodeRef,
+}: CustomTreeItemProps) => {
+ const { id, label, children, meta } = node;
+
+ const { id: menuId } = useParams();
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+
+ const [contextMenuRef, setContextMenuRef] = React.useState(null);
+
+ const [confirmedDelete, setConfirmedDelete] = React.useState(false);
+
+ const handleClickContextMenu = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setContextMenuRef(event.currentTarget as HTMLElement);
+ };
+ const handleCloseContextMenu = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setContextMenuRef(null);
+ };
+
+ const handleInsert = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setContextMenuRef(null);
+ const itemMeta = { ...meta };
+ data?.menu.meta?.forEach(m => {
+ const defaultValue = (meta || {})[m.id] || m.defaultValue;
+ switch (m.type) {
+ case MenuMetaType.TEXT:
+ case MenuMetaType.NUMBER:
+ case MenuMetaType.DATE:
+ itemMeta[m.name] = defaultValue || '';
+ break;
+ case MenuMetaType.BOOLEAN:
+ itemMeta[m.name] = defaultValue || false;
+ break;
+ }
+ });
+ const itemNode = {
+ id: -1,
+ label: t('menu.preview.newItem', {
+ order: children?.length ? children.length + 1 : 1,
+ }),
+ order: children?.length ? children.length + 1 : 1,
+ parentId: id,
+ meta: itemMeta,
+ enabled: true,
+ children: [],
+ startPublication: null,
+ endPublication: null,
+ };
+ setEditingNode({ ...itemNode, action: EnumInputAction.CREATE, original: itemNode });
+ setSelected('-1');
+ setOperationScreen(EnumInputActionScreen.INSERT);
+ };
+
+ const handleDelete = async (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setEditingNode({
+ ...node,
+ action: EnumInputAction.DELETE,
+ original: node,
+ });
+ setContextMenuRef(null);
+ // eslint-disable-next-line no-promise-executor-return
+ await new Promise(resolve => setTimeout(resolve, 100));
+ // eslint-disable-next-line no-alert
+ const confirmed = window.confirm(t('menu.preview.alerts.deleteItem.message'));
+ if (!confirmed) {
+ setEditingNode(emptyEditingNode);
+ // TODO: not working setSelected after cancel delete
+ setSelected(id.toString());
+ }
+ setConfirmedDelete(confirmed);
+ };
+
+ React.useEffect(() => {
+ if (confirmedDelete) {
+ handleUpdate();
+ }
+ }, [confirmedDelete, handleUpdate]);
+
+ const handleEditTemplate = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ navigate(`/menus/${menuId}/items/${id}`);
+ setContextMenuRef(null);
+ };
+
+ const onNodeClick = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ if (editingNode.id === -1) {
+ setSelected('-1');
+ return;
+ }
+ const itemMeta = { ...meta };
+ data?.menu.meta?.forEach(m => {
+ const defaultValue = (meta || {})[m.id] || m.defaultValue;
+ switch (m.type) {
+ case MenuMetaType.TEXT:
+ case MenuMetaType.NUMBER:
+ case MenuMetaType.DATE:
+ itemMeta[m.name] = defaultValue || '';
+ break;
+ case MenuMetaType.BOOLEAN:
+ itemMeta[m.name] = defaultValue || false;
+ break;
+ }
+ });
+ setEditingNode({
+ ...node,
+ meta: itemMeta,
+ action: EnumInputAction.UPDATE,
+ original: node,
+ });
+ setSelected(id.toString());
+ setOperationScreen(EnumInputActionScreen.UPDATE);
+ };
+
+ return (
+
+
+ {label}
+
+
+
+
+
+
+ }
+ sx={{
+ '& > .MuiTreeItem-content': {
+ color,
+ fontWeight,
+ border: '1px solid #eaeaec',
+ margin: '3px 0px',
+ backgroundColor: '#fff',
+ borderRadius: '4px',
+ padding: '12px 0px 13px 25px',
+ my: '0.5rem',
+ },
+ '& > .Mui-selected': {
+ border: '2px solid #3354FD',
+ backgroundColor: '#fff',
+ '&:hover': {
+ backgroundColor: 'rgba(0, 0, 0, 0.04)',
+ },
+ },
+ '& > .Mui-selected.Mui-focused': {
+ backgroundColor: '#fff',
+ '&:hover': {
+ backgroundColor: 'rgba(0, 0, 0, 0.04)',
+ },
+ },
+ maxWidth: '540px',
+ }}
+ >
+ {children?.length > 0 && renderNodes(children)}
+
+ );
+};
+
+const ArrowDownwardIcon = () => ;
interface Props {
nodes: INode[];
+ emptyEditingNode: IEditingNode;
editingNode: IEditingNode;
+ setEditingNode: (editingNode: IEditingNode) => void;
expanded: string[];
setExpanded: (expanded: string[]) => void;
selected: string;
setSelected: (selected: string) => void;
preview: (nodes: INode[], editingNode: IEditingNode) => INode[];
+ handleUpdate: () => Promise;
+ data: any;
+ setOperationScreen: (operationScreen: EnumInputActionScreen) => void;
}
export const NodeTreeView = ({
nodes,
+ emptyEditingNode,
editingNode,
+ setEditingNode,
expanded,
setExpanded,
selected,
setSelected,
preview,
+ handleUpdate,
+ data,
+ setOperationScreen,
}: Props) => {
+ const { t } = useTranslation();
+
+ const insertingNodeRef = React.useCallback(node => {
+ if (node) {
+ node.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, []);
+
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
setExpanded(nodeIds);
};
@@ -33,16 +290,8 @@ export const NodeTreeView = ({
};
const renderNodes = React.useCallback(
- (nodes: INode[]) => {
- const baseSX = {
- border: '1px solid #eaeaec',
- margin: '3px 0px',
- backgroundColor: '#fff',
- borderRadius: '4px',
- padding: '12px 0px 13px 25px',
- };
-
- return nodes.map(node => {
+ (nodes: INode[]) =>
+ nodes.map(node => {
let color = 'black';
const fontWeight = node.id === editingNode.id ? 'bold' : 'normal';
switch (node.action) {
@@ -56,43 +305,68 @@ export const NodeTreeView = ({
color = 'red';
break;
}
- if (node.children?.length > 0) {
- return (
- .MuiTreeItem-content > .MuiTreeItem-label': {
- ...baseSX,
- color,
- fontWeight,
- },
- }}
- >
- {renderNodes(node.children)}
-
- );
- }
return (
-
);
- });
- },
- [editingNode.id],
+ }),
+ [
+ emptyEditingNode,
+ editingNode,
+ handleUpdate,
+ setEditingNode,
+ setSelected,
+ data,
+ setOperationScreen,
+ insertingNodeRef,
+ ],
);
+ const handleInsertRoot = (event: React.SyntheticEvent) => {
+ const meta = {};
+ data?.menu.meta?.forEach(m => {
+ switch (m.type) {
+ case MenuMetaType.TEXT:
+ case MenuMetaType.NUMBER:
+ case MenuMetaType.DATE:
+ meta[m.name] = m.defaultValue || '';
+ break;
+ case MenuMetaType.BOOLEAN:
+ meta[m.name] = m.defaultValue || false;
+ break;
+ }
+ });
+ const node: INode = {
+ id: -1,
+ label: t('menu.preview.newItem', {
+ order: nodes.length ? nodes.length + 1 : 1,
+ }),
+ order: nodes.length ? nodes.length + 1 : 1,
+ parentId: 0,
+ meta,
+ enabled: true,
+ children: [],
+ startPublication: null,
+ endPublication: null,
+ };
+ setEditingNode({ ...node, action: EnumInputAction.CREATE, original: node });
+ setSelected('-1');
+ setOperationScreen(EnumInputActionScreen.INSERT);
+ };
+
return (
+
+
+
+ {t('menu.preview.actions.insertRoot')}
+
+
}
- defaultCollapseIcon={}
+ defaultExpandIcon={}
+ defaultCollapseIcon={}
defaultExpanded={['0']}
expanded={expanded}
selected={selected}
diff --git a/src/components/Menu/Items/OperationScreen.tsx b/src/components/Menu/Items/OperationScreen.tsx
index a0fca33..230ca18 100644
--- a/src/components/Menu/Items/OperationScreen.tsx
+++ b/src/components/Menu/Items/OperationScreen.tsx
@@ -11,29 +11,11 @@ import {
} from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DatePicker, DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
-import AddIcon from '@mui/icons-material/Add';
-import EditIcon from '@mui/icons-material/Edit';
-import DeleteIcon from '@mui/icons-material/Delete';
-import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined';
import { DateTime } from 'luxon';
-import { useMutation } from '@apollo/client';
import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-import {
- EnumInputAction,
- IEditingNode,
- IMenu,
- IMenuMeta,
- INode,
- MenuMetaType,
-} from '../../../types';
+import { IEditingNode, IMenu, IMenuMeta, INode, MenuMetaType } from '../../../types';
import { MENU_ITEM_VALIDATION } from '../../../constants';
-import {
- ActionTypes,
- NotificationContext,
- openDefaultErrorNotification,
-} from '../../../contexts/NotificationContext';
-import MenuService from '../../../api/services/MenuService';
+import { EnumInputActionScreen } from '../../../pages/Menu/Items';
const Form = styled('form')({
display: 'flex',
@@ -44,13 +26,6 @@ const Form = styled('form')({
overflowY: 'auto',
});
-enum EnumInputActionScreen {
- SELECTING_ACTION,
- INSERT,
- UPDATE,
- DELETE,
-}
-
interface Props {
id: string;
data: { menu: IMenu };
@@ -58,13 +33,10 @@ interface Props {
emptyEditingNode: IEditingNode;
editingNode: IEditingNode;
setEditingNode: (editingNode: IEditingNode) => void;
- expanded: string[];
- setExpanded: (expanded: string[]) => void;
- selected: string;
- setSelected: (selected: string) => void;
+ operationScreen: EnumInputActionScreen;
+ setOperationScreen: (operationScreen: EnumInputActionScreen) => void;
findNodeById: (nodes: INode[], id: number) => INode | undefined;
- preview: (nodes: INode[], editingNode: IEditingNode) => INode[];
- setUpdatedMenu: (menu: IMenu) => void;
+ handleUpdate: () => Promise;
}
export const OperationScreen = ({
@@ -73,151 +45,18 @@ export const OperationScreen = ({
nodes,
editingNode,
emptyEditingNode,
- expanded,
- setExpanded,
- selected,
- setSelected,
+ operationScreen,
+ setOperationScreen,
findNodeById,
- preview,
setEditingNode,
- setUpdatedMenu,
+ handleUpdate,
}: Props) => {
- const { dispatch } = React.useContext(NotificationContext);
-
- const navigate = useNavigate();
const { i18n, t } = useTranslation();
- const [operationScreen, setOperationScreen] = React.useState(
- EnumInputActionScreen.SELECTING_ACTION,
- );
const [labelError, setLabelError] = React.useState('');
const [startPublicationError, setStartPublicationError] = React.useState('');
const [endPublicationError, setEndPublicationError] = React.useState('');
- const [updateMenu] = useMutation(MenuService.UPDATE_MENU);
-
- const handleUpdate = async (): Promise => {
- const formatNodes = (nodes: INode[]) =>
- nodes
- .map(node => {
- const {
- id,
- children,
- original,
- parentId,
- createdAt,
- updatedAt,
- version,
- menuId,
- defaultTemplate,
- action,
- rules,
- ...rest
- } = node;
- let meta;
- if (rest.meta) {
- meta = Object.keys(rest.meta).reduce((acc, key) => {
- const name = data.menu.meta.find(meta => meta.id === Number(key))?.name || key;
- const meta = rest.meta[key];
- const originalMetaMap = original?.meta || {};
- const originalMeta = originalMetaMap[key] || originalMetaMap[name];
- if (meta == null || meta === '' || meta === originalMeta) {
- return acc;
- }
- return {
- ...acc,
- [name]: meta,
- };
- }, {});
- if (Object.keys(meta).length === 0) meta = undefined;
- }
- let startPublication;
- if (rest.startPublication) {
- if (
- (original?.startPublication !== rest.startPublication ||
- !original?.startPublication) &&
- rest.startPublication.isValid
- )
- startPublication = rest.startPublication.toISO();
- } else {
- startPublication = null;
- }
- let endPublication;
- if (rest.endPublication) {
- if (
- (original?.endPublication !== rest.endPublication || !original?.endPublication) &&
- rest.endPublication.isValid
- )
- endPublication = rest.endPublication.toISO();
- } else {
- endPublication = null;
- }
- let label;
- if (action === EnumInputAction.CREATE) label = rest.label;
- else if (rest.label && (original?.label !== rest.label || !original?.label))
- label = rest.label;
- let order;
- if (action === EnumInputAction.CREATE) order = rest.order;
- else if (rest.order && (original?.order !== rest.order || !original?.order))
- order = rest.order;
- let enabled = action === EnumInputAction.CREATE ? true : undefined;
- if (rest.enabled && (original?.enabled !== rest.enabled || !original?.enabled))
- enabled = rest.enabled;
- return {
- action,
- label,
- order,
- enabled,
- startPublication,
- endPublication,
- meta,
- rules,
- children: children && formatNodes(children),
- id: id === -1 ? undefined : id,
- };
- })
- .filter(node => !!node.action);
-
- const items = formatNodes(preview(nodes, editingNode)[0].children).map(node => {
- const id = node.id === -1 ? undefined : node.id;
- return {
- ...node,
- id,
- };
- });
-
- await updateMenu({
- variables: { menu: { id: Number(id), items } },
- onCompleted: data => {
- dispatch({
- type: ActionTypes.OPEN_NOTIFICATION,
- message: `${t('notification.editSuccess', {
- resource: t('menu.title', { count: 1 }),
- context: 'male',
- })}!`,
- });
- switch (editingNode.action) {
- case EnumInputAction.CREATE:
- case EnumInputAction.DELETE:
- setSelected('');
- break;
- case EnumInputAction.UPDATE:
- setSelected(editingNode.id.toString());
- break;
- }
- setUpdatedMenu(data.updateMenu);
- setOperationScreen(EnumInputActionScreen.SELECTING_ACTION);
- setEditingNode(emptyEditingNode);
- return Promise.resolve();
- },
- onError: error => {
- openDefaultErrorNotification(error, dispatch);
- return Promise.reject();
- },
- });
- return Promise.resolve();
- };
-
const handleInsertSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -273,113 +112,11 @@ export const OperationScreen = ({
}
};
- const handleDeleteSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- try {
- await handleUpdate();
- } catch (error) {
- console.error(error);
- }
+ const handleDiscard = () => {
+ setEditingNode(emptyEditingNode);
+ setOperationScreen(EnumInputActionScreen.NONE);
};
- const handleActionChange = React.useCallback(
- (action: EnumInputActionScreen) => {
- const selectedNode = findNodeById(nodes, Number(selected));
- if (expanded.find(id => id === selectedNode?.id.toString())) {
- setExpanded([...expanded, selectedNode.id.toString()]);
- }
- let node: INode;
- const meta = {};
- switch (action) {
- case EnumInputActionScreen.SELECTING_ACTION:
- setEditingNode(emptyEditingNode);
- break;
- case EnumInputActionScreen.INSERT:
- data?.menu.meta?.forEach(m => {
- switch (m.type) {
- case MenuMetaType.TEXT:
- case MenuMetaType.NUMBER:
- case MenuMetaType.DATE:
- meta[m.name] = m.defaultValue || '';
- break;
- case MenuMetaType.BOOLEAN:
- meta[m.name] = m.defaultValue || false;
- break;
- }
- });
- node = {
- id: -1,
- label: t('menu.preview.newItem', {
- order: selectedNode.children?.length ? selectedNode.children.length + 1 : 1,
- }),
- order: selectedNode.children?.length ? selectedNode.children.length + 1 : 1,
- parentId: selectedNode.id,
- meta,
- enabled: true,
- children: [],
- startPublication: null,
- endPublication: null,
- };
- setEditingNode({ ...node, action: EnumInputAction.CREATE, original: node });
- setSelected('-1');
- break;
- case EnumInputActionScreen.UPDATE:
- if (!selectedNode || selectedNode.id === 0) {
- !selectedNode && setSelected('');
- return;
- }
- data?.menu.meta?.forEach(m => {
- const defaultValue = (selectedNode.meta || {})[m.id] || m.defaultValue;
- switch (m.type) {
- case MenuMetaType.TEXT:
- case MenuMetaType.NUMBER:
- case MenuMetaType.DATE:
- meta[m.name] = defaultValue || '';
- break;
- case MenuMetaType.BOOLEAN:
- meta[m.name] = defaultValue || false;
- break;
- }
- });
- setEditingNode({
- ...selectedNode,
- meta,
- action: EnumInputAction.UPDATE,
- original: selectedNode,
- });
- setSelected(selectedNode.id.toString());
- break;
- case EnumInputActionScreen.DELETE:
- if (!selectedNode || selectedNode.id === 0) {
- !selectedNode && setSelected('');
- return;
- }
- setEditingNode({
- ...selectedNode,
- action: EnumInputAction.DELETE,
- original: selectedNode,
- });
- setSelected(selectedNode.id.toString());
- break;
- }
- setOperationScreen(action);
- },
- [
- data,
- findNodeById,
- nodes,
- selected,
- t,
- expanded,
- emptyEditingNode,
- setEditingNode,
- setExpanded,
- setSelected,
- ],
- );
-
const renderMeta = () => {
const renderInput = (meta: IMenuMeta) => {
switch (meta.type) {
@@ -519,139 +256,6 @@ export const OperationScreen = ({
};
switch (operationScreen) {
- case EnumInputActionScreen.SELECTING_ACTION:
- return (
-
-
- {t('menu.preview.actions.title')}
-
- {!selected && (
-
- {t('menu.preview.errors.noItemSelected')}
-
- )}
-
-
-
-
-
-
-
- );
case EnumInputActionScreen.INSERT:
return (
@@ -990,16 +556,6 @@ export const OperationScreen = ({
>
{t('menu.preview.actions.edit')}
-
{
const parent = findNodeById(nodes, editingNode.parentId);
- const order = Math.min(
- Math.max(Number(e.target.value), 1),
- parent.children?.length ? parent.children.length : 1,
- );
+ let maxOrder = 1;
+ if (parent?.children?.length) {
+ maxOrder = parent.children.length;
+ } else if (nodes.length) {
+ maxOrder = nodes.length;
+ }
+ const order = Math.min(Math.max(Number(e.target.value), 1), maxOrder);
if (order === editingNode.order) return;
setEditingNode({
...editingNode,
@@ -1257,81 +816,12 @@ export const OperationScreen = ({
>
{t('buttons.save')}
-