diff --git a/app/components/FeatureFlags/flags.js b/app/components/FeatureFlags/flags.js index 4f5953081..4f54270b7 100644 --- a/app/components/FeatureFlags/flags.js +++ b/app/components/FeatureFlags/flags.js @@ -15,5 +15,7 @@ export const FLAGS = { PLATFORM_FEATURES_OPT_IN: "platform-feature-opt-in-feature-flag", EVENT_NUDGE_FEATURE_FLAG_KEY : "user-event-nudge-feature-flag", CUSTOMIZE_NAVIGATION_MENU: "customize-navigation-menu-feature-flag", + DRAGGABLE_NAVIGATION_ITEMS: "draggable-navigation-items-feature-flag", + DROPDOWN_VIEW_FOR_NAV_CONTROL : "dropdown-view-for-nav-items-feature-flag", ...USER_PORTAL_FLAGS }; diff --git a/app/containers/MassEnergizeSuperAdmin/ME Tools/dropdown/MEDropdownPro.js b/app/containers/MassEnergizeSuperAdmin/ME Tools/dropdown/MEDropdownPro.js index 8cc7462a9..0256976e7 100644 --- a/app/containers/MassEnergizeSuperAdmin/ME Tools/dropdown/MEDropdownPro.js +++ b/app/containers/MassEnergizeSuperAdmin/ME Tools/dropdown/MEDropdownPro.js @@ -1,7 +1,5 @@ import { Checkbox, Chip, FormControlLabel } from "@mui/material"; -import React, { - useEffect, useState, useCallback, useRef -} from "react"; +import React, { useEffect, useState, useCallback, useRef } from "react"; import { pop } from "../../../../utils/common"; import { apiCall } from "../../../../utils/messenger"; @@ -15,6 +13,9 @@ function MEDropdownPro({ placeholder, defaultValue, value, + noCaret = false, + renderChild, + renderLabel, ...rest }) { const [selected, setSelected] = useState(defaultValue || value || []); @@ -22,7 +23,6 @@ function MEDropdownPro({ const [cursor, setCursor] = React.useState({ has_more: true, next: 1 }); const [optionsToDisplay, setOptionsToDisplay] = useState(data || []); - // ------------------------------------------------------------------- const elementObserver = useRef(null); const lastDropDownItemRef = useCallback( @@ -31,29 +31,21 @@ function MEDropdownPro({ elementObserver.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && cursor.has_more) { if (!rest?.endpoint) return; - apiCall(rest?.endpoint, { page: cursor.next, limit: 10 }).then( - (res) => { - setCursor({ - has_more: res?.cursor?.count > optionsToDisplay?.length, - next: res?.cursor?.next, - }); - const items = [ - ...optionsToDisplay, - ...(res?.data || [])?.map((item) => ({ - ...item, - displayName: labelExtractor - ? labelExtractor(item) - : item?.name || item?.title, - })), - ]; + apiCall(rest?.endpoint, { page: cursor.next, limit: 10 }).then((res) => { + setCursor({ + has_more: res?.cursor?.count > optionsToDisplay?.length, + next: res?.cursor?.next + }); + const items = [ + ...optionsToDisplay, + ...(res?.data || [])?.map((item) => ({ + ...item, + displayName: labelExtractor ? labelExtractor(item) : item?.name || item?.title + })) + ]; - setOptionsToDisplay([ - ...new Map( - items.map((item) => [item.id, item]) - ).values(), - ]); - } - ); + setOptionsToDisplay([...new Map(items.map((item) => [item.id, item])).values()]); + }); } }); @@ -111,48 +103,37 @@ function MEDropdownPro({ }; const itemIsSelected = (item) => { - const found = (selected || []).find( - (it) => it.toString() === item.toString() - ); + const found = (selected || []).find((it) => it.toString() === item.toString()); return found; }; const renderChildren = () => { if (!show) return <>; - return optionsToDisplay?.map((d, i) => ( -

handleOnChange(d)} - className="drop-pro-child" - style={{ ...(multiple ? { padding: '13px' } : {}), ...(d.style || {}) }} - > - {multiple && ( - - )} - {labelOf(d)} -

- )); + return optionsToDisplay?.map((d, i) => + renderChild ? ( + renderChild(d, i) + ) : ( +

handleOnChange(d)} + className="drop-pro-child" + style={{ ...(multiple ? { padding: "13px" } : {}), ...(d.style || {}) }} + > + {multiple && } + {renderLabel ? renderLabel({ item: d, selected }) : labelOf(d)} +

+ ) + ); }; return (
setShow(!show)}> {renderHeader()} - + {!noCaret && }
- {show && ( -
setShow(false)} - /> - )} - {show && ( -
{renderChildren()}
- )} + {show &&
setShow(false)} />} + {show &&
{renderChildren()}
}
); } diff --git a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js index 4c843bcbb..fc828feca 100644 --- a/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js +++ b/app/containers/MassEnergizeSuperAdmin/Pages/CustomNavigationConfiguration.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, 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"; @@ -14,12 +14,16 @@ import Loading from "dan-components/Loading"; import MEDropdown from "../ME Tools/dropdown/MEDropdown"; import { apiCall } from "../../../utils/messenger"; import { fetchParamsFromURL, smartString } from "../../../utils/common"; +import { FLAGS } from "../../../components/FeatureFlags/flags"; +import Feature from "../../../components/FeatureFlags/Feature"; const NAVIGATION = "navigation"; const FOOTER = "footer"; const BRAND = "brand"; const INIT = "INIT"; const RESET = "RESET"; +const UP = "up"; +const DOWN = "down"; const ACTIVITIES = { edit: { key: "edit", description: "This item was edited, and unsaved!", color: "#fffcf3" }, @@ -52,9 +56,18 @@ 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 [dropZone, setDropZone] = useState(null); const menuHeap = useSelector((state) => state.getIn(["menuConfigurations"])); + const communities = useSelector((state) => state.getIn(["communities"])); const { comId: community_id } = fetchParamsFromURL(window.location, "comId"); + const community = useMemo(() => communities?.find((c) => c?.id?.toString() === community_id?.toString(), []), [ + communities + ]); + // const community = communities?.find((c) => c?.id?.toString() === community_id?.toString(), []); const dispatch = useDispatch(); const keepInRedux = (menuProfiles, options) => dispatch( @@ -105,6 +118,113 @@ function CustomNavigationConfiguration() { placeDetails(menuObj); }, []); + // Add mouse move handler + useEffect(() => { + const handler = (e) => { + setMouse([e.x, e.y]); + }; + document.addEventListener("mousemove", handler); + return () => document.removeEventListener("mousemove", handler); + }, []); + + // Track closest drop-zone to dragged item + useEffect(() => { + if (dragged !== null) { + // get all drop-zones + const elements = Array.from(document.getElementsByClassName("nav-drop-zone")); + const parentIds = elements.map((e) => e.getAttribute("data-parent-ids")); + const indexes = elements.map((e) => e.getAttribute("data-index")); + const where = elements.map((e) => e.getAttribute("data-position")); + const idsOfItems = elements.map((e) => e.getAttribute("data-id")); + // get all drop-zones' y-axis position + const positions = elements.map((e) => e.getBoundingClientRect().top); + // get the difference with the mouse's y position + const absDifferences = positions.map((v) => Math.abs(v - mouse[1])); + // get the index of the dropzone closest to the mouse + let result = absDifferences.indexOf(Math.min(...absDifferences)); + const placement = where[result]; + setDropZone({ + parentIds: parentIds[result], + index: indexes[result], // The index of the item that is currently at the position that we are trying to drop the dragged item + uniqueId: `${parentIds[result]}->${indexes[result]}->${placement}`, + id: idsOfItems[result], + placement //up or down : Helps determine whether to add or subtract from index + }); + } + }, [dragged, mouse]); + + useEffect(() => { + const handler = (e) => { + if (!dragged) return; + e.preventDefault(); + setBeingDragged(null); + reorder(); + }; + document.addEventListener("mouseup", handler); + return () => document.removeEventListener("mouseup", handler); + }); + + /** + * + * @param {*} dragObject - Item that is set in the state when drage starts + * @param {*} list - original list of menu items + * @returns + */ + const removeDraggedItem = (dragObject, list) => { + if (!dragObject) return list; + const { item, parents } = dragObject; + const parentsArr = Object.values(parents); + const isTopLevelItem = parentsArr?.length === 0; + if (isTopLevelItem) return list.filter((m) => m?.id !== item?.id); + const immediateParent = parentsArr[parentsArr.length - 1]; + let sibblings = immediateParent?.children || []; + sibblings = sibblings.filter((s) => s?.id !== item?.id); + parents[(immediateParent?.id)] = { ...immediateParent, children: sibblings }; + const newObj = rollUp(Object.entries(parents)); + return insertIntoTopLevelList(newObj, list); + }; + const unWrapTo = (parentIdList, mother) => { + const tracker = { [mother?.id]: { ...mother, children: [...(mother?.children || [])] } }; + parentIdList = parentIdList.slice(1); + let current = mother; + for (let i = 0; i < parentIdList.length; i++) { + const key = parentIdList[i]; + const found = current?.children?.find((m) => m?.id === key); + tracker[(found?.id)] = found; + current = found; + } + return tracker; + }; + + const dragIntoNewPosition = (positionInformation, topLevelList) => { + const { parentIds, index, placement } = positionInformation; + const newPosition = placement === UP ? index : Number(index) + 1; + const pIds = (parentIds?.split(":") || []).filter(Boolean); + const isTopLevelItem = pIds.length === 0; + if (isTopLevelItem) { + const sibblings = [...topLevelList]; + sibblings.splice(newPosition, 0, dragged?.item); + return sibblings; + } + const mother = topLevelList?.find((m) => m?.id === pIds[0]); + const parentsAsObject = unWrapTo(pIds, mother); + const immediateParent = Object.values(parentsAsObject)[pIds.length - 1]; + const sibblings = immediateParent?.children || []; + sibblings.splice(newPosition, 0, dragged?.item); + immediateParent.children = sibblings; + parentsAsObject[(immediateParent?.id)] = immediateParent; + const newObj = rollUp(Object.entries(parentsAsObject)); + return insertIntoTopLevelList(newObj, topLevelList); + }; + + const reorder = () => { + if (!dropZone || dropZone?.id === dragged?.item?.id) return; + // Dragged item has been removed, top level list is modified, and ready for insertion + const listAfterDraggedIsRemoved = removeDraggedItem(dragged, [...menuItems]); + const listAfterDragInsertion = dragIntoNewPosition(dropZone, listAfterDraggedIsRemoved); + setStateAndExport(listAfterDragInsertion, trackEdited); + }; + const updateForm = (key, value, reset = false) => { if (reset) return setForm({}); setForm({ ...form, [key]: value }); @@ -186,15 +306,22 @@ function CustomNavigationConfiguration() { } 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); + const insertIntoTopLevelList = (obj, array) => { + array = [...array]; + const ind = array.findIndex((m) => m?.id === obj?.id); + if (ind === -1) array.push(obj); + else array[ind] = obj; + return array; + }; + const setStateAndExport = (newState, changeTree = null) => { + setMenu(newState); + const profileList = recreateProfileFromList(newState); keepInRedux(profileList, { changeTree }); }; + const addToTopLevelMenu = (obj, changeTree = null) => { + const copied = insertIntoTopLevelList(obj, menuItems); + setStateAndExport(copied, changeTree); + }; const removeItem = (itemObj, parents, options = {}) => { closeModal(); @@ -248,19 +375,28 @@ function CustomNavigationConfiguration() { }); }; + const combineKeysWithDelimiter = (keys, delimiter = ":") => keys.join(delimiter); 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; + const isBeingDragged = dragged?.item?.id === rest?.id; + + const parentKeys = combineKeysWithDelimiter(Object.keys(parents)); + const isFirstItem = index === 0; return ( -
+
{margin ? : <>} moveUp(up, { ...rest, children }, parents, { index, sibblings: items })} @@ -425,7 +562,29 @@ function CustomNavigationConfiguration() { background: "#fafafa" }} > -
{renderMenuItems(menuItems)}
+
+ {dragged !== null && ( +
+ + {dragged?.item?.name} +
+ )} + + {renderMenuItems(menuItems)} +