From 3859550aca25242c9f9a5e9f822119ce5eacb45a Mon Sep 17 00:00:00 2001 From: frimpongopoku Date: Thu, 18 Jul 2024 23:47:59 +0000 Subject: [PATCH 01/14] Simulating dragging works. Left with drop zones --- .../CustomNavigationConfiguration-Backup.js | 659 ++++++++++++++++++ .../Pages/CustomNavigationConfiguration.js | 44 +- app/styles/ME Custom/extra.css | 15 +- 3 files changed, 712 insertions(+), 6 deletions(-) create mode 100644 app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration-Backup.js diff --git a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration-Backup.js b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration-Backup.js new file mode 100644 index 000000000..4c843bcbb --- /dev/null +++ b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration-Backup.js @@ -0,0 +1,659 @@ +import React, { useEffect, useState } from "react"; +import MEPaperBlock from "../ME Tools/paper block/MEPaperBlock"; +import { Button, Link, Paper, TextField, Tooltip, Typography } from "@mui/material"; +import BrandCustomization from "./BrandCustomization"; +import { useDispatch, useSelector } from "react-redux"; +import { + reduxAddMenuConfiguration, + reduxToggleUniversalModal, + reduxToggleUniversalToast +} from "../../../redux/redux-actions/adminActions"; +import CreateAndEditMenu, { INTERNAL_LINKS } from "./CreateAndEditMenu"; +import { EXAMPLE_MENU_STRUCTURE } from "../ME Tools/media library/shared/utils/values"; +import Loading from "dan-components/Loading"; +import MEDropdown from "../ME Tools/dropdown/MEDropdown"; +import { apiCall } from "../../../utils/messenger"; +import { fetchParamsFromURL, smartString } from "../../../utils/common"; + +const NAVIGATION = "navigation"; +const FOOTER = "footer"; +const BRAND = "brand"; +const INIT = "INIT"; +const RESET = "RESET"; + +const ACTIVITIES = { + edit: { key: "edit", description: "This item was edited, and unsaved!", color: "#fffcf3" }, + add: { key: "add", description: "This item is new, and unsaved!", color: "#f4fff4" }, + remove: { key: "remove", description: "This item is removed, but changes are not saved yet!", color: "#ffeded" } +}; +const LComponent = () => { + return ( +
+ ); +}; +function CustomNavigationConfiguration() { + const [menuItems, setMenu] = useState([]); + const [form, setForm] = useState({}); + const [trackEdited, setEdited] = useState({}); + const [status, setLoadingStatus] = useState({}); + const [activeStash, setActiveStash] = useState({}); + const [menuProfileStash, stashMenuProfiles] = useState([]); + const [error, setError] = useState(null); + const [brandForm, setBrandForm] = useState({}); + + const menuHeap = useSelector((state) => state.getIn(["menuConfigurations"])); + const { comId: community_id } = fetchParamsFromURL(window.location, "comId"); + const dispatch = useDispatch(); + const keepInRedux = (menuProfiles, options) => + dispatch( + reduxAddMenuConfiguration({ + ...menuHeap, + [community_id]: { data: menuProfiles, ...(options || {}) } + }) + ); + + const recreateProfileFromList = (updatedList) => { + const profile = { ...activeStash, content: updatedList }; + const rem = menuProfileStash.filter((m) => dm?.id !== profile?.id); + return [...rem, profile]; + }; + + const placeDetails = (menuObject) => { + setMenu(menuObject?.content || []); + setActiveStash(menuObject); + gatherBrandInfo(menuObject); + // set footer details too here... + }; + + const loadContent = () => { + setLoading(INIT, true); + apiCall("menus.listForAdmins", { community_id }) + .then((response) => { + setLoading(INIT, false); + if (!response?.success) return setError(response?.message); + const menuProfiles = response?.data || []; + // dispatch(reduxAddMenuConfiguration({ ...menuHeap, [community_id]: data })); + stashMenuProfiles(menuProfiles); + keepInRedux(menuProfiles); + const activeMenu = menuProfiles[0]; + placeDetails(activeMenu); + }) + .catch((er) => { + setLoading(INIT, false); + setError(er?.toString()); + }); + }; + + useEffect(() => { + const reduxObj = (menuHeap || {})[community_id]; + const menuProfiles = reduxObj?.data || []; + if (!menuProfiles?.length) return loadContent(); + const menuObj = menuProfiles[0]; + setEdited(reduxObj?.changeTree || {}); + placeDetails(menuObj); + }, []); + + const updateForm = (key, value, reset = false) => { + if (reset) return setForm({}); + setForm({ ...form, [key]: value }); + }; + const setLoading = (key, value) => setLoadingStatus({ ...status, [key]: value }); + const toggleModal = (props) => dispatch(reduxToggleUniversalModal(props)); + + const notify = (message, success = false) => { + dispatch(reduxToggleUniversalToast({ open: true, message, variant: success ? "success" : "error" })); + }; + const closeModal = () => toggleModal({ show: false, component: null }); + + const assembleIntoObject = (parentObj, child) => { + if (!child) return parentObj; + const index = parentObj?.children?.findIndex((p) => p?.id === child?.id); + if (index > -1) parentObj.children[index] = child; + return parentObj; + }; + + const createChangeObject = (changedVersion, options = {}) => { + return { data: { ...changedVersion }, activity: options?.context }; + }; + + const resetActivityContext = () => { + // setItemBeforeEdit({}); + // setActivityContext(null); + }; + + const hasChanged = (oldObj, newObj) => { + const keys = Object.keys(oldObj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (oldObj[key] !== newObj[key]) return true; + } + }; + + const trackChanges = (changedVersion, options) => { + const { context, itemBefore } = options || {}; + // Checks if an item has been edited, and if it has, it marks it as edited + // When a new item is added or one is removed, it is marked as such + if (context === ACTIVITIES.edit.key) { + const itemHasChanged = hasChanged(changedVersion, itemBefore); + if (!itemHasChanged) return resetActivityContext(); + } + const newChangeObject = { ...trackEdited, [changedVersion?.id]: createChangeObject(changedVersion, options) }; + setEdited(newChangeObject); + return newChangeObject; + // resetActivityContext(); + }; + + const insertNewLink = (linkObj, parents, options = {}) => { + closeModal(); + let newObj = linkObj; + parents = Object.entries(parents); + const dealingWithAChild = parents.length > 0; + if (dealingWithAChild) { + const lastIndex = parents.length - 1; + const [key, obj] = parents[lastIndex]; + const siblings = [...(obj?.children || [])]; + const index = siblings.findIndex((s) => s?.id === linkObj?.id); + if (index > -1) siblings[index] = { ...linkObj }; + else siblings.push({ ...linkObj }); + parents[lastIndex] = [key, { ...obj, children: [...siblings] }]; + newObj = rollUp(parents); + } + const newChanges = trackChanges(linkObj, options); + addToTopLevelMenu(newObj, newChanges); + // trackChanges(linkObj, options); + }; + + const rollUp = (parents) => { + const reversed = [...parents].reverse(); + let acc = reversed[0][1]; + for (let i = 0; i < reversed.length; i++) { + if (i === reversed.length - 1) break; // Exit the loop when reaching the end of the array + const nextIndex = i + 1; + const next = reversed[nextIndex]; + acc = assembleIntoObject(next[1], acc); + } + return acc; + }; + const addToTopLevelMenu = (obj, changeTree = null) => { + const ind = menuItems.findIndex((m) => m?.id === obj?.id); + const copied = [...menuItems]; + if (ind === -1) copied.push(obj); + else copied[ind] = obj; + setMenu(copied); + const profileList = recreateProfileFromList(copied); + keepInRedux(profileList, { changeTree }); + }; + + const removeItem = (itemObj, parents, options = {}) => { + closeModal(); + parents = Object.entries(parents); + const dealingWithAChild = parents.length > 0; + if (!dealingWithAChild) { + const newMenu = menuItems.filter((m) => m?.id !== itemObj?.id); + // trackChanges(itemObj, { ...options, context: ACTIVITIES.remove.key }); + return setMenu(newMenu); + } + let parentAsObj = itemObj; + const lastIndex = parents.length - 1; + const [id, immediateParent] = parents[lastIndex]; + let family = immediateParent?.children || []; + family = family.filter((f) => f?.id !== itemObj?.id); + parents[lastIndex] = [id, { ...immediateParent, children: [...family] }]; + //if you want to remove the item from the state, uncomment the code below + parentAsObj = rollUp(parents); + addToTopLevelMenu(parentAsObj); + // trackChanges(itemObj, { ...options, context: ACTIVITIES.remove.key }); + }; + + const resetToDefault = () => { + toggleModal({ + show: true, + title: "Reset to default", + component:
Are you sure you want to reset the menu to the default configuration?
, + onConfirm: () => makeRequestToReset(), + onCancel: () => closeModal() + }); + }; + + const addOrEdit = (itemObj, parents = {}, options = {}) => { + // setItemBeforeEdit(itemObj); + const { children, isEdit } = options || {}; + toggleModal({ + show: true, + noTitle: true, + fullControl: true, + component: ( + insertNewLink(obj, parents, { ...options, itemBefore: itemObj })} + updateForm={updateForm} + data={itemObj} + /> + ) + }); + }; + + const renderMenuItems = (items, margin = 0, parents = {}, options = {}) => { + if (!items?.length) return []; + // items = items.sort((a, b) => a?.order - b?.order); //If you want to sort the items by order, uncomment this line + return items.map(({ children, ...rest }, index) => { + const { parentTraits } = options || {}; + const editTrail = trackEdited[(rest?.id)]; + let activity = editTrail ? ACTIVITIES[(editTrail?.activity)] : null; + const isRemoved = activity?.key === ACTIVITIES.remove.key; + + return ( +
+ {margin ? : <>} + moveUp(up, { ...rest, children }, parents, { index, sibblings: items })} + /> + {/* -- I'm spreading "children" here to make sure that we create a copy of the children. We want to make sure we control when the changes show up for the user */} + {children && + renderMenuItems( + children, + 40, + { ...parents, [rest?.id]: { ...rest, children: [...children] } }, + { parentTraits: { isRemoved, isPublished: rest?.is_published, isChild: true }, ...(options || {}) } + )} +
+ ); + }); + }; + + const moveUp = (up, item, parents = {}, options = {}) => { + let { index, sibblings } = options || {}; + const newIndex = up ? index - 1 : index + 1; + parents = Object.entries(parents); + const dealingWithAChild = parents.length > 0; + if (!dealingWithAChild) { + sibblings = [...sibblings]; + sibblings = sibblings.filter((s, i) => i !== index); + sibblings.splice(newIndex, 0, item); + return setMenu(sibblings); + } + + const lastIndex = parents.length - 1; + const [id, immediateParent] = parents[lastIndex]; + let family = [...(immediateParent?.children || [])]; + family = family.filter((f) => f?.id !== item?.id); + family.splice(newIndex, 0, item); + parents[lastIndex] = [id, { ...immediateParent, children: [...family] }]; + const parentAsObj = rollUp(parents); + addToTopLevelMenu(parentAsObj, trackEdited); + }; + + const gatherBrandInfo = (profile) => { + const { community_logo_link, community } = profile || {}; + setBrandForm({ link: community_logo_link, media: [community?.logo] }); + }; + const makeRequestToReset = () => { + setLoading(NAVIGATION, true); + closeModal(); + apiCall("menus.reset", { id: activeStash?.id }) + .then((response) => { + setLoading(NAVIGATION, false); + const data = response?.data; + if (!response?.success) return notify(response?.error); + const profiles = [data]; + placeDetails(data); + keepInRedux(profiles, { changeTree: null }); + setEdited({}); + notify("Menu reset successful!", true); + }) + .catch((er) => { + setLoading(NAVIGATION, false); + notify(er?.toString()); + }); + }; + + const pushChangesToBackend = (data, scope) => { + setLoading(scope, true); + const [media] = brandForm?.media || []; + const form = { + id: activeStash?.id, + community_logo_link: brandForm?.link, + community_logo_id: media?.id, + ...(data || {}) + }; + + apiCall("menus.update", form) + .then((response) => { + setLoading(scope, false); + if (!response?.success) return notify(response?.error); + let data = response?.data; + if (scope === BRAND) { + notify(`Details updated successfully`, true); + gatherBrandInfo(data); + const { content, footer_content, ...rest } = data; + keepInRedux([{ ...activeStash, ...rest }], { changeTree: trackEdited }); + return; + } + notify(`Menu updated successfully`, true); + const profiles = [data]; + placeDetails(data); + keepInRedux(profiles, { changeTree: null }); + setEdited({}); + }) + .catch((er) => { + setLoading(scope, false); + notify(er?.toString()); + }); + }; + const pageIsLoading = status[INIT]; + const isLoading = status[NAVIGATION]; + const isResetting = status[RESET]; + + if (pageIsLoading) return ; + if (error) + return ( + + + {error} + + + ); + + const addNew = () => + addOrEdit({ id: new Date().getTime()?.toString(), is_published: true }, {}, { context: ACTIVITIES.add.key }); + + return ( +
+ + {/*

This is what the custom navigation configuration page will look like

*/} + pushChangesToBackend(null, BRAND)} + onChange={(key, value) => setBrandForm({ ...(brandForm || {}), [key]: value })} + form={brandForm} + /> +
+ + + + Customize your site's navigation here. You can edit, remove and + addNew()} + className="touchable-opacity" + style={{ fontWeight: "bold", color: "var(--app-purple)", marginLeft: 5 }} + > + + Add new menu items{" "} + + here + +
+
{renderMenuItems(menuItems)}
+
+
+ +
+
+ +
+
+ + +
+ +
+ ); +} + +export default CustomNavigationConfiguration; + +const OneMenuItem = ({ + performDeletion, + addOrEdit, + children, + openModal, + item, + parents, + activity, + parentTraits, + isTheFirstItem, + isTheLastItem, + moveUp +}) => { + const { name, link, id, is_link_external, is_published } = item || {}; + + const parentIsNotLive = !parentTraits?.isPublished; + const isChild = parentTraits?.isChild; + const hasChildren = children?.length > 0; + const getBackColor = () => { + if (activity) return activity?.color; + if (parentTraits?.isRemoved) return ACTIVITIES.remove.color; + return "white"; + }; + const removeMenuItem = () => { + // const hasChildren = children?.length > 0; + let message = `Are you sure you want to remove "${item?.name}" from the menu?`; + if (hasChildren) + message = `If you remove "${item?.name}", all it's (${children?.length || + ""}) children will be removed. Are you sure you want to continue?`; + openModal({ + show: true, + title: "Delete Confirmation", + component:
{message}
, + onConfirm: () => performDeletion(item, parents) + }); + }; + + const parentsForNewItem = { ...(parents || {}), [item?.id]: { ...item, children: [...(children || [])] } }; + + const isRemoved = parentTraits?.isRemoved || activity?.key === ACTIVITIES.remove.key; + const editItem = () => + addOrEdit({ ...item, children }, parents, { context: ACTIVITIES.edit.key, children, isEdit: true }); + + const itemIsLive = () => { + if (isChild) return !parentIsNotLive && is_published; + return is_published; + }; + + const disabledBecauseOfParent = parentIsNotLive && is_published; + + return ( +
+ + {!isTheFirstItem && ( + + moveUp(true)} + className=" fa fa-long-arrow-up touchable-opacity" + style={{ color: "var(--app-cyan)", marginRight: 10, fontSize: 20 }} + /> + + )} + {!isTheLastItem && ( + moveUp(false)} title={`Move down`}> + + + )} + + {name} + + {is_link_external && !hasChildren && ( + + + EXT + + + )} + {!children?.length && link && ( + { + e.preventDefault(); + if (is_link_external) return window.open(link, "_blank"); + }} + // href={link} + target="_blank" + > + + {smartString(link, 40)} + + {is_link_external && } + + + )} + + {!isRemoved && ( +
+ + editItem()} + className={`fa fa-eye${itemIsLive() ? "" : "-slash"} touchable-opacity`} + style={{ marginRight: 20, color: itemIsLive() ? "var(--app-purple)" : "grey", fontSize: 20 }} + /> + + + + addOrEdit({ id: new Date().getTime()?.toString(), is_published: true }, parentsForNewItem, { + context: ACTIVITIES.add.key + }) + } + className=" fa fa-plus touchable-opacity" + style={{ marginRight: 20, color: "green", fontSize: 20 }} + /> + + + editItem()} + className=" fa fa-edit touchable-opacity" + style={{ fontSize: 20, color: "var(--app-cyan)" }} + /> + + | + + removeMenuItem()} + className=" fa fa-trash touchable-opacity" + style={{ color: "#e87070", marginRight: 10, fontSize: 20 }} + /> + +
+ )} +
+ ); +}; diff --git a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js index 4c843bcbb..f4479d49c 100644 --- a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js +++ b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js @@ -52,6 +52,9 @@ function CustomNavigationConfiguration() { const [menuProfileStash, stashMenuProfiles] = useState([]); const [error, setError] = useState(null); const [brandForm, setBrandForm] = useState({}); + // ----- For Dragging and Dropping ------ + const [dragged, setBeingDragged] = useState(null); + const [mouse, setMouse] = useState([]); const menuHeap = useSelector((state) => state.getIn(["menuConfigurations"])); const { comId: community_id } = fetchParamsFromURL(window.location, "comId"); @@ -105,6 +108,16 @@ function CustomNavigationConfiguration() { placeDetails(menuObj); }, []); + useEffect(() => { + const handler = (e) => { + setMouse([e.x, e.y]); + }; + + document.addEventListener("mousemove", handler); + + return () => document.removeEventListener("mousemove", handler); + }, []); + const updateForm = (key, value, reset = false) => { if (reset) return setForm({}); setForm({ ...form, [key]: value }); @@ -257,10 +270,15 @@ function CustomNavigationConfiguration() { let activity = editTrail ? ACTIVITIES[(editTrail?.activity)] : null; const isRemoved = activity?.key === ACTIVITIES.remove.key; + if (dragged?.id === rest?.id) return <>; + + // return renderOneItem({ ...rest, children }, { index, options, parents, margin }); + return (
{margin ? : <>} -
{renderMenuItems(menuItems)}
+
+ {dragged !== null && ( +
+ {dragged?.name} +
+ )} + {renderMenuItems(menuItems)} +
-
-
- -
-
- - -
- -
- ); -} - -export default CustomNavigationConfiguration; - -const OneMenuItem = ({ - performDeletion, - addOrEdit, - children, - openModal, - item, - parents, - activity, - parentTraits, - isTheFirstItem, - isTheLastItem, - moveUp -}) => { - const { name, link, id, is_link_external, is_published } = item || {}; - - const parentIsNotLive = !parentTraits?.isPublished; - const isChild = parentTraits?.isChild; - const hasChildren = children?.length > 0; - const getBackColor = () => { - if (activity) return activity?.color; - if (parentTraits?.isRemoved) return ACTIVITIES.remove.color; - return "white"; - }; - const removeMenuItem = () => { - // const hasChildren = children?.length > 0; - let message = `Are you sure you want to remove "${item?.name}" from the menu?`; - if (hasChildren) - message = `If you remove "${item?.name}", all it's (${children?.length || - ""}) children will be removed. Are you sure you want to continue?`; - openModal({ - show: true, - title: "Delete Confirmation", - component:
{message}
, - onConfirm: () => performDeletion(item, parents) - }); - }; - - const parentsForNewItem = { ...(parents || {}), [item?.id]: { ...item, children: [...(children || [])] } }; - - const isRemoved = parentTraits?.isRemoved || activity?.key === ACTIVITIES.remove.key; - const editItem = () => - addOrEdit({ ...item, children }, parents, { context: ACTIVITIES.edit.key, children, isEdit: true }); - - const itemIsLive = () => { - if (isChild) return !parentIsNotLive && is_published; - return is_published; - }; - - const disabledBecauseOfParent = parentIsNotLive && is_published; - - return ( -
- - {!isTheFirstItem && ( - - moveUp(true)} - className=" fa fa-long-arrow-up touchable-opacity" - style={{ color: "var(--app-cyan)", marginRight: 10, fontSize: 20 }} - /> - - )} - {!isTheLastItem && ( - moveUp(false)} title={`Move down`}> - - - )} - - {name} - - {is_link_external && !hasChildren && ( - - - EXT - - - )} - {!children?.length && link && ( - { - e.preventDefault(); - if (is_link_external) return window.open(link, "_blank"); - }} - // href={link} - target="_blank" - > - - {smartString(link, 40)} - - {is_link_external && } - - - )} - - {!isRemoved && ( -
- - editItem()} - className={`fa fa-eye${itemIsLive() ? "" : "-slash"} touchable-opacity`} - style={{ marginRight: 20, color: itemIsLive() ? "var(--app-purple)" : "grey", fontSize: 20 }} - /> - - - - addOrEdit({ id: new Date().getTime()?.toString(), is_published: true }, parentsForNewItem, { - context: ACTIVITIES.add.key - }) - } - className=" fa fa-plus touchable-opacity" - style={{ marginRight: 20, color: "green", fontSize: 20 }} - /> - - - editItem()} - className=" fa fa-edit touchable-opacity" - style={{ fontSize: 20, color: "var(--app-cyan)" }} - /> - - | - - removeMenuItem()} - className=" fa fa-trash touchable-opacity" - style={{ color: "#e87070", marginRight: 10, fontSize: 20 }} - /> - -
- )} -
- ); -}; From 71d5447b9cc5cc10b7505624216399a6b8fb34a8 Mon Sep 17 00:00:00 2001 From: frimpongopoku Date: Thu, 25 Jul 2024 11:12:23 +0000 Subject: [PATCH 14/14] Remove commented code --- .../Pages/CustomNavigationConfiguration.js | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js index 8d8cd9e0e..fc828feca 100644 --- a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js +++ b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js @@ -417,15 +417,6 @@ function CustomNavigationConfiguration() { index={index} moveUp={(up) => moveUp(up, { ...rest, children }, parents, { index, sibblings: items })} /> - {/*
${index}->up` ? "nav-hidden" : "" - }`} - data-parent-ids={parentKeys} - data-index={index} - data-position="up" - /> */} {/* -- I'm spreading "children" here to make sure that we create a copy of the children. We want to make sure we control when the changes show up for the user */} {children && renderMenuItems( @@ -591,12 +582,6 @@ function CustomNavigationConfiguration() { {dragged?.item?.name}
)} - {/*
topmost" ? "nav-hidden" : ""}`} - className={`nav-drop-zone ${!dragged ? "nav-hidden" : ""}`} - data-parent-ids={""} - data-index={"topmost"} - /> */} {renderMenuItems(menuItems)}
@@ -731,12 +716,6 @@ const OneMenuItem = ({ { key: "remove", label: "Remove", icon: "fa-trash", color: "rgb(227 151 151)", onClick: removeMenuItem } ]; - // const dropdownItems = [ - // { key: "edit", label: "Edit", icon: "fa-edit", color: "rgb(117 154 210)", onClick: editItem }, - // { key: "add", label: "Add Child Item", icon: "fa-plus", color: "rgb(104 180 95)", onClick: createNewSubItem }, - // { key: "remove", label: "Remove", icon: "fa-trash", color: "rgb(227 151 151)", onClick: removeMenuItem } - // ]; - return ( <> {isTheFirstItem && notInTheSamePosition && ( @@ -873,11 +852,6 @@ const OneMenuItem = ({ {renderLiveVisuals()} - // addOrEdit({ id: new Date().getTime()?.toString(), is_published: true }, parentsForNewItem, { - // context: ACTIVITIES.add.key - // }) - // } onClick={() => createNewSubItem()} className=" fa fa-plus touchable-opacity" style={{ marginRight: 20, color: "green", fontSize: 20 }}