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} + + + + + + + + {t('menu.preview.actions.insertChild')} + + + + {t('menu.preview.actions.editTemplate')} + + + {t('buttons.delete')} + + + + } + 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 (
@@ -669,42 +273,6 @@ export const OperationScreen = ({ > {t('menu.preview.actions.insert')} - - - - { const parent = findNodeById(nodes, editingNode.parentId); - const order = Math.min( - Math.max(Number(e.target.value), 1), - parent.children?.length ? parent.children.length + 1 : 1, - ); + let maxOrder = 1; + if (parent?.children?.length) { + maxOrder = parent.children.length + 1; + } else if (nodes.length) { + maxOrder = nodes.length + 1; + } + const order = Math.min(Math.max(Number(e.target.value), 1), maxOrder); if (order === editingNode.order) return; setEditingNode({ ...editingNode, @@ -962,12 +533,7 @@ export const OperationScreen = ({ > {t('buttons.save')} - @@ -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')} - ); - case EnumInputActionScreen.DELETE: - return ( - - - {t('menu.preview.actions.delete')} - - - - - - - - - ); default: return null; } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 28f9f69..97ee0a0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -259,7 +259,9 @@ "insert": "Insert", "edit": "Edit", "delete": "Delete", - "editTemplate": "Edit Template" + "editTemplate": "Edit Template", + "insertChild": "Insert Child", + "insertRoot": "New Menu Item" }, "parent": "Parent", "newItem": "New Item {{order}}", @@ -283,6 +285,12 @@ }, "errors": { "noItemSelected": "No item selected" + }, + "alerts": { + "deleteItem": { + "title": "Delete Item", + "message": "Are you sure you want to delete this item?" + } } }, "list": { diff --git a/src/i18n/locales/pt-BR/translation.json b/src/i18n/locales/pt-BR/translation.json index 2f2daa7..2e1a5ab 100644 --- a/src/i18n/locales/pt-BR/translation.json +++ b/src/i18n/locales/pt-BR/translation.json @@ -260,7 +260,9 @@ "insert": "Inserir", "edit": "Editar", "delete": "Deletar", - "editTemplate": "Editar Template" + "editTemplate": "Editar Template", + "insertChild": "Inserir Item Filho", + "insertRoot": "Novo Item de Menu" }, "parent": "Item Pai", "newItem": "Novo Item {{order}}", @@ -284,6 +286,12 @@ }, "errors": { "noItemSelected": "Nenhum item selecionado" + }, + "alerts": { + "deleteItem": { + "title": "Deletar Item", + "message": "Tem certeza que deseja deletar esse item?" + } } }, "list": { diff --git a/src/pages/Menu/Edit/Edit.tsx b/src/pages/Menu/Edit/Edit.tsx index 183d081..b5389cd 100644 --- a/src/pages/Menu/Edit/Edit.tsx +++ b/src/pages/Menu/Edit/Edit.tsx @@ -260,7 +260,7 @@ export const EditMenu = () => {
- + @@ -301,7 +301,7 @@ export const EditMenu = () => { action={FormAction.UPDATE} /> - + diff --git a/src/pages/Menu/Items/EditTemplate.tsx b/src/pages/Menu/Items/EditTemplate.tsx index 9d1168f..67abbeb 100644 --- a/src/pages/Menu/Items/EditTemplate.tsx +++ b/src/pages/Menu/Items/EditTemplate.tsx @@ -286,7 +286,7 @@ export const EditTemplateItems = () => { items={[ { label: t('menu.title', { count: 2 }), navigateTo: '/' }, { label: data?.menuItem.menu.name, navigateTo: '../../' }, - { label: t('menu.preview.title'), navigateTo: '../edit' }, + { label: t('menu.preview.title'), navigateTo: '../../edit' }, { label: data?.menuItem.label }, ]} onBack={onBackClickHandler} diff --git a/src/pages/Menu/Items/Items.tsx b/src/pages/Menu/Items/Items.tsx index dfce96c..48bdcfb 100644 --- a/src/pages/Menu/Items/Items.tsx +++ b/src/pages/Menu/Items/Items.tsx @@ -1,9 +1,8 @@ -import { Box, Typography } from '@mui/material'; +import { Box } from '@mui/material'; import React, { useCallback, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; -import WidgetsIcon from '@mui/icons-material/Widgets'; import { useTranslation } from 'react-i18next'; -import { useQuery } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client'; import { DateTime } from 'luxon'; import Loading from '../../../components/Loading'; import MenuService from '../../../api/services/MenuService'; @@ -17,6 +16,11 @@ import { } from '../../../types'; import DefaultErrorPage from '../../../components/DefaultErrorPage'; import { NodeTreeView, OperationScreen } from '../../../components/Menu/Items'; +import { + ActionTypes, + NotificationContext, + openDefaultErrorNotification, +} from '../../../contexts/NotificationContext'; const emptyEditingNode: IEditingNode = { id: 0, @@ -39,7 +43,15 @@ const emptyEditingNode: IEditingNode = { }, }; +export enum EnumInputActionScreen { + NONE, + INSERT, + UPDATE, +} + export const ItemsPreview = () => { + const { dispatch } = React.useContext(NotificationContext); + const { t } = useTranslation(); const { id } = useParams(); @@ -47,12 +59,17 @@ export const ItemsPreview = () => { const { loading, error, data } = useQuery(MenuService.GET_MENU, { variables: { id: Number(id) }, }); + const [updateMenu] = useMutation(MenuService.UPDATE_MENU); const [updatedMenu, setUpdatedMenu] = useState(); const [expanded, setExpanded] = useState(['0']); const [selected, setSelected] = useState('0'); + const [operationScreen, setOperationScreen] = React.useState( + EnumInputActionScreen.NONE, + ); + const nodes = useMemo(() => { const items: GraphQLData[] = updatedMenu?.items || data?.menu.items || []; const getChildren = (parent: IMenuItem): INode[] => { @@ -103,17 +120,8 @@ export const ItemsPreview = () => { }) .filter(item => !item.parentId) .sort((a, b) => a.order - b.order) || []; - return [ - { - id: 0, - label: t('menu.preview.root'), - order: 1, - enabled: true, - children: root, - meta: {}, - }, - ]; - }, [updatedMenu, data?.menu, t]); + return root; + }, [updatedMenu, data?.menu]); const [editingNode, setEditingNode] = useState(emptyEditingNode); @@ -129,10 +137,132 @@ export const ItemsPreview = () => { return undefined; }, []); + 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)).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: + setEditingNode(emptyEditingNode); + setSelected(''); + break; + case EnumInputAction.UPDATE: + setSelected(editingNode.id.toString()); + break; + } + setUpdatedMenu(data.updateMenu); + setOperationScreen(EnumInputActionScreen.NONE); + setEditingNode(emptyEditingNode); + return Promise.resolve(); + }, + onError: error => { + openDefaultErrorNotification(error, dispatch); + return Promise.reject(); + }, + }); + return Promise.resolve(); + }; + const preview = useCallback((nodes: INode[], editingNode: IEditingNode): INode[] => { if (!editingNode.id) return nodes; - if (nodes[0].id === 0 && nodes[0].children?.length === 0) - return [{ ...nodes[0], children: [editingNode] }]; + if (nodes.length === 0) return [editingNode]; // console.log('nodes', nodes); // console.log('editing node', editingNode); const nodesPreview: INode[] = []; @@ -144,6 +274,109 @@ export const ItemsPreview = () => { return { ...child, action: EnumInputAction.DELETE }; }); }; + const reorder = (nodes: INode[]): INode[] => { + const siblings = nodes.map(sibling => { + if (sibling.id === editingNode.id) { + // sibling is editing node + // console.log('child is editing node', child); + if (sibling.order === editingNode.order) { + // sibling is in same position + // console.log('sibling is in same position'); + if (editingNode.action === EnumInputAction.DELETE) { + // editing node is DELETE + // console.log('editing node is DELETE'); + // set sibling as DELETE + return { + ...sibling, + action: EnumInputAction.DELETE, + children: deleteChildren(sibling), + }; + } + // editing node is UPDATE + // console.log('editing node is UPDATE'); + // set sibling as UPDATE + return { + ...sibling, + ...editingNode, + action: EnumInputAction.UPDATE, + }; + } + // sibling is in different position + // console.log('sibling is in different position'); + // set sibling as UPDATE and set order as editing node order + return { + ...sibling, + ...editingNode, + action: EnumInputAction.UPDATE, + }; + } + // sibling is not editing node + if (sibling.order === editingNode.order) { + // sibling is in same position as editing node + // console.log('sibling is in same position as editing node', sibling); + // set order based on existing node order diff (moving up or down) + const diff = editingNode.order - editingNode.original.order; + let order = diff >= 0 ? sibling.order - 1 : sibling.order + 1; + if (editingNode.action === EnumInputAction.DELETE) { + // editing node is DELETE + // console.log('editing node is DELETE'); + // decrease order + order = sibling.order - 1; + } + // set sibling as UPDATE and decrease order + return { ...sibling, action: EnumInputAction.UPDATE, order }; + } + if (sibling.order > editingNode.order) { + // sibling is after editing node + // console.log('sibling is after editing node', sibling); + // set sibling as UPDATE + let action = EnumInputAction.UPDATE; + let { order } = sibling; + if (editingNode.action === EnumInputAction.UPDATE) { + if (sibling.order < editingNode.original.order) { + // sibling order is less than editing node original order + // increase sibling order + order += 1; + } else { + // sibling order is greater than editing node original order + // keep sibling order as is and set action as undefined + action = undefined; + } + } else if (editingNode.action === EnumInputAction.DELETE) { + order -= 1; + } else if (editingNode.action === EnumInputAction.CREATE) { + order += 1; + } + return { ...sibling, order, action }; + } + // sibling is before editing node + // console.log('sibling is before editing node', sibling); + if ( + editingNode.action === EnumInputAction.UPDATE && + sibling.order > editingNode.original.order + ) { + // sibling order is greater than editing node original order + // decrease sibling order + return { ...sibling, action: EnumInputAction.UPDATE, order: sibling.order - 1 }; + } + return sibling; + }); + return siblings; + }; + if (editingNode.parentId === 0) { + // editing node is root + // console.log('editing node is root'); + // reordering root + let root = reorder(nodes); + if (editingNode.action === EnumInputAction.CREATE) { + // editing node is CREATE + // console.log('editing node is CREATE'); + // add editing node to root + root.splice(editingNode.order - 1, 0, editingNode); + } + root = root.sort((a, b) => a.order - b.order); + return root; + } nodes.forEach(node => { // console.log('current node', node); if (node.id === editingNode.id) { @@ -181,92 +414,10 @@ export const ItemsPreview = () => { action: EnumInputAction.UPDATE, }); } else { - let children = node.children.map(child => { - if (child.id === editingNode.id) { - // child is editing node - // console.log('child is editing node', child); - if (child.order === editingNode.order) { - // child is in same position - // console.log('child is in same position'); - if (editingNode.action === EnumInputAction.DELETE) { - // editing node is DELETE - // console.log('editing node is DELETE'); - // set child as DELETE - return { - ...child, - action: EnumInputAction.DELETE, - children: deleteChildren(child), - }; - } - // editing node is UPDATE - // console.log('editing node is UPDATE'); - // set child as UPDATE - return { - ...child, - ...editingNode, - action: EnumInputAction.UPDATE, - }; - } - // child is in different position - // console.log('child is in different position'); - // set child as UPDATE and set order as editing node order - return { - ...child, - ...editingNode, - action: EnumInputAction.UPDATE, - }; - } - // child is not editing node - if (child.order === editingNode.order) { - // child is in same position as editing node - // console.log('child is in same position as editing node', child); - // set order based on existing node order diff (moving up or down) - const diff = editingNode.order - editingNode.original.order; - let order = diff >= 0 ? child.order - 1 : child.order + 1; - if (editingNode.action === EnumInputAction.DELETE) { - // editing node is DELETE - // console.log('editing node is DELETE'); - // decrease order - order = child.order - 1; - } - // set child as UPDATE and decrease order - return { ...child, action: EnumInputAction.UPDATE, order }; - } - if (child.order > editingNode.order) { - // child is after editing node - // console.log('child is after editing node', child); - // set child as UPDATE - let action = EnumInputAction.UPDATE; - let { order } = child; - if (editingNode.action === EnumInputAction.UPDATE) { - if (child.order < editingNode.original.order) { - // child order is less than editing node original order - // increase child order - order += 1; - } else { - // child order is greater than editing node original order - // keep child order as is and set action as undefined - action = undefined; - } - } else if (editingNode.action === EnumInputAction.DELETE) { - order -= 1; - } else if (editingNode.action === EnumInputAction.CREATE) { - order += 1; - } - return { ...child, order, action }; - } - // child is before editing node - // console.log('child is before editing node', child); - if ( - editingNode.action === EnumInputAction.UPDATE && - child.order > editingNode.original.order - ) { - // child order is greater than editing node original order - // decrease child order - return { ...child, action: EnumInputAction.UPDATE, order: child.order - 1 }; - } - return child; - }); + // parent has children + // console.log('parent has children'); + // reordering children + let children = reorder(node.children); if (editingNode.action === EnumInputAction.CREATE) { // editing node is CREATE // console.log('editing node is CREATE'); @@ -327,11 +478,11 @@ export const ItemsPreview = () => { ); return ( - + { selected={selected} setSelected={setSelected} preview={preview} + emptyEditingNode={emptyEditingNode} + setEditingNode={setEditingNode} + handleUpdate={handleUpdate} + data={data} + setOperationScreen={setOperationScreen} /> + {operationScreen !== EnumInputActionScreen.NONE ? ( + <> +
-
- - + + + ) : null}