diff --git a/src/config/sidebarMenu.js b/src/config/sidebarMenu.js index e3b11386b..605504b3e 100644 --- a/src/config/sidebarMenu.js +++ b/src/config/sidebarMenu.js @@ -1,5 +1,6 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; +import { Features } from '../common/features'; import { ISidebarMenuItemType, ISidebarMenuOverlayIds, @@ -74,6 +75,7 @@ export const SidebarMenu = [ text: , href: '/warehouses-transfers', type: ISidebarMenuItemType.Link, + feature: Features.Warehouses }, { text: , @@ -105,6 +107,7 @@ export const SidebarMenu = [ ), href: '/warehouses-transfers/new', type: ISidebarMenuItemType.Link, + feature: Features.Warehouses }, { text: , diff --git a/src/containers/Dashboard/Sidebar/hooks.js b/src/containers/Dashboard/Sidebar/hooks.js index 28e69817a..a5e56839e 100644 --- a/src/containers/Dashboard/Sidebar/hooks.js +++ b/src/containers/Dashboard/Sidebar/hooks.js @@ -1,29 +1,31 @@ import _, { isEmpty } from 'lodash'; import React from 'react'; -import deepdash from 'deepdash'; import * as R from 'ramda'; import { useHistory } from 'react-router-dom'; import { useAbilityContext } from 'hooks'; -import { useSidebarSubmenu } from 'hooks/state'; +import { useSidebarSubmenu, useFeatureCan } from 'hooks/state'; import { SidebarMenu } from 'config/sidebarMenu'; import { ISidebarMenuItemType } from './interfaces'; import { useSidebarSubmnuActions, useDialogActions } from 'hooks/state'; - -const deep = deepdash(_); +import { filterValuesDeep, deepdash } from 'utils'; const deepDashConfig = { childrenPath: 'children', pathFormat: 'array', }; +const ingoreTypesEmpty = [ + ISidebarMenuItemType.Group, + ISidebarMenuItemType.Overlay, +]; /** - * - * @param {*} menu - * @returns + * Removes the all overlay items from the menu to the main-sidebar. + * @param {ISidebarMenuItem[]} menu + * @returns {ISidebarMenuItem[]} */ function removeSidebarOverlayChildren(menu) { - return deep.mapValuesDeep( + return deepdash.mapValuesDeep( menu, (item, key, parent, context) => { if (item.type === ISidebarMenuItemType.Overlay) { @@ -38,33 +40,54 @@ function removeSidebarOverlayChildren(menu) { /** * Retrives the main sidebar pre-menu. - * @returns + * @returns {ISidebarMenuItem[]} */ export function getMainSidebarMenu() { return R.compose(removeSidebarOverlayChildren)(SidebarMenu); } -/** - * Retrieves the sub sidebar pre-menu. - */ -export function getSubSidebarMenu() {} +function useFilterSidebarItemFeaturePredicater() { + const { featureCan } = useFeatureCan(); -/** - * - * @param {*} menu - * @returns - */ -function useFilterSidebarMenuAbility(menu) { + return { + predicate: (item) => { + if (item.feature && !featureCan(item.feature)) { + return false; + } + return true; + }, + }; +} + +function useFilterSidebarItemAbilityPredicater() { const ability = useAbilityContext(); - return deep.filterDeep( + return { + predicate: (item) => { + if ( + item.permission && + !ability.can(item.permission.ability, item.permission.subject) + ) { + return false; + } + return true; + }, + }; +} + +/** + * Filters sidebar menu items based on ability of the item permission. + * @param {*} menu + * @returns {} + */ +function useFilterSidebarMenuAbility(menu) { + const { predicate: predFeature } = useFilterSidebarItemFeaturePredicater(); + const { predicate: predAbility } = useFilterSidebarItemAbilityPredicater(); + + return deepdash.filterDeep( menu, (item) => { - return ( - (item.permission && - ability.can(item.permission.ability, item.permission.subject)) || - !item.permission - ); + return predFeature(item) && predAbility(item); }, deepDashConfig, ); @@ -77,44 +100,103 @@ function useFilterSidebarMenuAbility(menu) { */ function useFlatSidebarMenu(menu) { return React.useMemo(() => { - return deep.mapDeep(menu, (item) => item, deepDashConfig); + return deepdash.mapDeep(menu, (item) => item, deepDashConfig); }, [menu]); } /** - * - * @param {*} menu - * @returns + * Binds sidebar link item click action. + * @param {ISidebarMenuItem} item */ -function useBindSidebarItemLinkClick(menu) { +function useBindSidebarItemLinkClick() { const history = useHistory(); - const { toggleSidebarSubmenu, closeSidebarSubmenu } = - useSidebarSubmnuActions(); + const { closeSidebarSubmenu } = useSidebarSubmnuActions(); + // Handle sidebar item click. + const onClick = (item) => (event) => { + closeSidebarSubmenu(); + history.push(item.href); + }; + return { + bindOnClick: (item) => { + return { + ...item, + onClick: onClick(item), + }; + }, + }; +} + +/** + * Bind sidebar dialog item click action. + * @param {ISidebarMenuItem} item + */ +function useBindSidebarItemDialogClick() { + const { closeSidebarSubmenu } = useSidebarSubmnuActions(); const { openDialog } = useDialogActions(); // Handle sidebar item click. - const handleClick = (item) => (event) => { + const onClick = (item) => (event) => { closeSidebarSubmenu(); - - // - if (item.type === ISidebarMenuItemType.Overlay) { - toggleSidebarSubmenu({ submenuId: item.overlayId }); - // - } else if (item.type === ISidebarMenuItemType.Link) { - history.push(item.href); - } else if (item.type === ISidebarMenuItemType.Dialog) { - openDialog(item.dialogName, item.dialogPayload); - } + openDialog(item.dialogName, item.dialogPayload); }; + return { + bindOnClick: (item) => { + return { + ...item, + onClick: onClick(item), + }; + }, + }; +} + +function useBindSidebarItemOverlayClick() { + const { toggleSidebarSubmenu, closeSidebarSubmenu } = + useSidebarSubmnuActions(); + + // Handle sidebar item click. + const onClick = (item) => (event) => { + closeSidebarSubmenu(); + toggleSidebarSubmenu({ submenuId: item.overlayId }); + }; + return { + bindOnClick: (item) => { + return { + ...item, + onClick: onClick(item), + }; + }, + }; +} + +/** + * + * @param {} menu + * @returns {} + */ +function useBindSidebarItemClick(menu) { + const { bindOnClick: bindLinkClickEvt } = useBindSidebarItemLinkClick(); + const { bindOnClick: bindOverlayClickEvt } = useBindSidebarItemOverlayClick(); + const { bindOnClick: bindItemDialogEvt } = useBindSidebarItemDialogClick(); + return React.useMemo(() => { - return deep.mapValuesDeep( + return deepdash.mapValuesDeep( menu, (item) => { - return { - ...item, - onClick: handleClick(item), - }; + return R.compose( + R.when( + R.propSatisfies(R.equals(ISidebarMenuItemType.Link), 'type'), + bindLinkClickEvt, + ), + R.when( + R.propSatisfies(R.equals(ISidebarMenuItemType.Overlay), 'type'), + bindOverlayClickEvt, + ), + R.when( + R.propSatisfies(R.equals(ISidebarMenuItemType.Dialog), 'type'), + bindItemDialogEvt, + ), + )(item); }, deepDashConfig, ); @@ -122,10 +204,13 @@ function useBindSidebarItemLinkClick(menu) { } /** - * + * Finds the given overlay submenu id from the menu graph. + * @param {ISidebarMenuOverlayIds} + * @param {ISidebarMenuItem[]} menu - + * @returns {ISidebarMenuItem[]} */ const findSubmenuBySubmenuId = R.curry((submenuId, menu) => { - const groupItem = deep.findDeep( + const groupItem = deepdash.findDeep( menu, (item) => { return ( @@ -140,25 +225,26 @@ const findSubmenuBySubmenuId = R.curry((submenuId, menu) => { /** * Retrieves the main sidebar post-menu. + * @returns {ISidebarMenuItem[]} */ export function useMainSidebarMenu() { return R.compose( - useBindSidebarItemLinkClick, + useBindSidebarItemClick, useFlatSidebarMenu, - removeSidebarOverlayChildren, useAssocSidebarItemHasChildren, + filterSidebarItemHasNoChildren, useFilterSidebarMenuAbility, )(SidebarMenu); } /** * Assoc `hasChildren` prop to sidebar menu items. - * @param {*} items - * @returns + * @param {ISidebarMenuItem[]} items + * @returns {ISidebarMenuItem[]} */ function useAssocSidebarItemHasChildren(items) { - return deep.mapValuesDeep( + return deepdash.mapValuesDeep( items, (item) => { return { @@ -172,15 +258,16 @@ function useAssocSidebarItemHasChildren(items) { /** * Retrieves the sub-sidebar post-menu. - * @param {*} submenuId - * @returns + * @param {ISidebarMenuOverlayIds} submenuId + * @returns {ISidebarMenuItem[]} */ export function useSubSidebarMenu(submenuId) { if (!submenuId) return []; return R.compose( - useBindSidebarItemLinkClick, + useBindSidebarItemClick, useFlatSidebarMenu, + filterSidebarItemHasNoChildren, useFilterSidebarMenuAbility, findSubmenuBySubmenuId(submenuId), )(SidebarMenu); @@ -202,8 +289,22 @@ export function useObserveSidebarExpendedBodyclass(sidebarExpended) { */ export function useIsSidebarMenuItemActive(item) { const { submenuId } = useSidebarSubmenu(); - return ( item.type === ISidebarMenuItemType.Overlay && submenuId === item.overlayId ); } + +/** + * Filter sidebar specific items types that have no types. + * @param {ISidebarMenuItem[]} items - + * @returns {ISidebarMenuItem[]} + */ +export function filterSidebarItemHasNoChildren(items) { + return filterValuesDeep((item) => { + // If it was group item and has no children items so discard that item. + if (ingoreTypesEmpty.indexOf(item.type) !== -1 && isEmpty(item.children)) { + return false; + } + return true; + }, items); +} diff --git a/src/utils/deep.js b/src/utils/deep.js new file mode 100644 index 000000000..500c49b07 --- /dev/null +++ b/src/utils/deep.js @@ -0,0 +1,33 @@ +import _ from 'lodash'; +import Deepdash from 'deepdash'; + +export const deepdash = Deepdash(_); + +export const filterValuesDeep = (predicate, nodes) => { + return deepdash.condense( + deepdash.reduceDeep( + nodes, + (accumulator, value, key, parent, context) => { + const newValue = { ...value }; + + if (newValue.children) { + _.set(newValue, 'children', deepdash.condense(value.children)); + } + const isTrue = predicate(newValue, key, parent, context); + + if (isTrue === true) { + _.set(accumulator, context.path, newValue); + } else if (isTrue === false) { + _.unset(accumulator, context.path); + } + return accumulator; + }, + [], + { + childrenPath: 'children', + pathFormat: 'array', + callbackAfterIterate: true, + }, + ), + ); +}; diff --git a/src/utils/index.js b/src/utils/index.js index c2800a416..2a756b20b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,6 +14,8 @@ import { isEqual } from 'lodash'; import jsCookie from 'js-cookie'; +export * from './deep'; + export const getCookie = (name, defaultValue) => _.defaultTo(jsCookie.get(name), defaultValue);