mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
feature(Sidebar): BIG-374 filtering the sidebar items based on each item feature support.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage as T } from 'components';
|
import { FormattedMessage as T } from 'components';
|
||||||
|
import { Features } from '../common/features';
|
||||||
import {
|
import {
|
||||||
ISidebarMenuItemType,
|
ISidebarMenuItemType,
|
||||||
ISidebarMenuOverlayIds,
|
ISidebarMenuOverlayIds,
|
||||||
@@ -74,6 +75,7 @@ export const SidebarMenu = [
|
|||||||
text: <T id={'sidebar_warehouse_transfer'} />,
|
text: <T id={'sidebar_warehouse_transfer'} />,
|
||||||
href: '/warehouses-transfers',
|
href: '/warehouses-transfers',
|
||||||
type: ISidebarMenuItemType.Link,
|
type: ISidebarMenuItemType.Link,
|
||||||
|
feature: Features.Warehouses
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'category_list'} />,
|
text: <T id={'category_list'} />,
|
||||||
@@ -105,6 +107,7 @@ export const SidebarMenu = [
|
|||||||
),
|
),
|
||||||
href: '/warehouses-transfers/new',
|
href: '/warehouses-transfers/new',
|
||||||
type: ISidebarMenuItemType.Link,
|
type: ISidebarMenuItemType.Link,
|
||||||
|
feature: Features.Warehouses
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'New service'} />,
|
text: <T id={'New service'} />,
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import _, { isEmpty } from 'lodash';
|
import _, { isEmpty } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import deepdash from 'deepdash';
|
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAbilityContext } from 'hooks';
|
import { useAbilityContext } from 'hooks';
|
||||||
import { useSidebarSubmenu } from 'hooks/state';
|
import { useSidebarSubmenu, useFeatureCan } from 'hooks/state';
|
||||||
import { SidebarMenu } from 'config/sidebarMenu';
|
import { SidebarMenu } from 'config/sidebarMenu';
|
||||||
import { ISidebarMenuItemType } from './interfaces';
|
import { ISidebarMenuItemType } from './interfaces';
|
||||||
import { useSidebarSubmnuActions, useDialogActions } from 'hooks/state';
|
import { useSidebarSubmnuActions, useDialogActions } from 'hooks/state';
|
||||||
|
import { filterValuesDeep, deepdash } from 'utils';
|
||||||
const deep = deepdash(_);
|
|
||||||
|
|
||||||
const deepDashConfig = {
|
const deepDashConfig = {
|
||||||
childrenPath: 'children',
|
childrenPath: 'children',
|
||||||
pathFormat: 'array',
|
pathFormat: 'array',
|
||||||
};
|
};
|
||||||
|
const ingoreTypesEmpty = [
|
||||||
|
ISidebarMenuItemType.Group,
|
||||||
|
ISidebarMenuItemType.Overlay,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Removes the all overlay items from the menu to the main-sidebar.
|
||||||
* @param {*} menu
|
* @param {ISidebarMenuItem[]} menu
|
||||||
* @returns
|
* @returns {ISidebarMenuItem[]}
|
||||||
*/
|
*/
|
||||||
function removeSidebarOverlayChildren(menu) {
|
function removeSidebarOverlayChildren(menu) {
|
||||||
return deep.mapValuesDeep(
|
return deepdash.mapValuesDeep(
|
||||||
menu,
|
menu,
|
||||||
(item, key, parent, context) => {
|
(item, key, parent, context) => {
|
||||||
if (item.type === ISidebarMenuItemType.Overlay) {
|
if (item.type === ISidebarMenuItemType.Overlay) {
|
||||||
@@ -38,33 +40,54 @@ function removeSidebarOverlayChildren(menu) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrives the main sidebar pre-menu.
|
* Retrives the main sidebar pre-menu.
|
||||||
* @returns
|
* @returns {ISidebarMenuItem[]}
|
||||||
*/
|
*/
|
||||||
export function getMainSidebarMenu() {
|
export function getMainSidebarMenu() {
|
||||||
return R.compose(removeSidebarOverlayChildren)(SidebarMenu);
|
return R.compose(removeSidebarOverlayChildren)(SidebarMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function useFilterSidebarItemFeaturePredicater() {
|
||||||
* Retrieves the sub sidebar pre-menu.
|
const { featureCan } = useFeatureCan();
|
||||||
*/
|
|
||||||
export function getSubSidebarMenu() {}
|
|
||||||
|
|
||||||
/**
|
return {
|
||||||
*
|
predicate: (item) => {
|
||||||
* @param {*} menu
|
if (item.feature && !featureCan(item.feature)) {
|
||||||
* @returns
|
return false;
|
||||||
*/
|
}
|
||||||
function useFilterSidebarMenuAbility(menu) {
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFilterSidebarItemAbilityPredicater() {
|
||||||
const ability = useAbilityContext();
|
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,
|
menu,
|
||||||
(item) => {
|
(item) => {
|
||||||
return (
|
return predFeature(item) && predAbility(item);
|
||||||
(item.permission &&
|
|
||||||
ability.can(item.permission.ability, item.permission.subject)) ||
|
|
||||||
!item.permission
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
deepDashConfig,
|
deepDashConfig,
|
||||||
);
|
);
|
||||||
@@ -77,44 +100,103 @@ function useFilterSidebarMenuAbility(menu) {
|
|||||||
*/
|
*/
|
||||||
function useFlatSidebarMenu(menu) {
|
function useFlatSidebarMenu(menu) {
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
return deep.mapDeep(menu, (item) => item, deepDashConfig);
|
return deepdash.mapDeep(menu, (item) => item, deepDashConfig);
|
||||||
}, [menu]);
|
}, [menu]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Binds sidebar link item click action.
|
||||||
* @param {*} menu
|
* @param {ISidebarMenuItem} item
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
function useBindSidebarItemLinkClick(menu) {
|
function useBindSidebarItemLinkClick() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { toggleSidebarSubmenu, closeSidebarSubmenu } =
|
const { closeSidebarSubmenu } = useSidebarSubmnuActions();
|
||||||
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();
|
const { openDialog } = useDialogActions();
|
||||||
|
|
||||||
// Handle sidebar item click.
|
// Handle sidebar item click.
|
||||||
const handleClick = (item) => (event) => {
|
const onClick = (item) => (event) => {
|
||||||
closeSidebarSubmenu();
|
closeSidebarSubmenu();
|
||||||
|
openDialog(item.dialogName, item.dialogPayload);
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
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 React.useMemo(() => {
|
||||||
return deep.mapValuesDeep(
|
return deepdash.mapValuesDeep(
|
||||||
menu,
|
menu,
|
||||||
(item) => {
|
(item) => {
|
||||||
return {
|
return R.compose(
|
||||||
...item,
|
R.when(
|
||||||
onClick: handleClick(item),
|
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,
|
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 findSubmenuBySubmenuId = R.curry((submenuId, menu) => {
|
||||||
const groupItem = deep.findDeep(
|
const groupItem = deepdash.findDeep(
|
||||||
menu,
|
menu,
|
||||||
(item) => {
|
(item) => {
|
||||||
return (
|
return (
|
||||||
@@ -140,25 +225,26 @@ const findSubmenuBySubmenuId = R.curry((submenuId, menu) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the main sidebar post-menu.
|
* Retrieves the main sidebar post-menu.
|
||||||
|
* @returns {ISidebarMenuItem[]}
|
||||||
*/
|
*/
|
||||||
export function useMainSidebarMenu() {
|
export function useMainSidebarMenu() {
|
||||||
return R.compose(
|
return R.compose(
|
||||||
useBindSidebarItemLinkClick,
|
useBindSidebarItemClick,
|
||||||
useFlatSidebarMenu,
|
useFlatSidebarMenu,
|
||||||
|
|
||||||
removeSidebarOverlayChildren,
|
removeSidebarOverlayChildren,
|
||||||
useAssocSidebarItemHasChildren,
|
useAssocSidebarItemHasChildren,
|
||||||
|
filterSidebarItemHasNoChildren,
|
||||||
useFilterSidebarMenuAbility,
|
useFilterSidebarMenuAbility,
|
||||||
)(SidebarMenu);
|
)(SidebarMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assoc `hasChildren` prop to sidebar menu items.
|
* Assoc `hasChildren` prop to sidebar menu items.
|
||||||
* @param {*} items
|
* @param {ISidebarMenuItem[]} items
|
||||||
* @returns
|
* @returns {ISidebarMenuItem[]}
|
||||||
*/
|
*/
|
||||||
function useAssocSidebarItemHasChildren(items) {
|
function useAssocSidebarItemHasChildren(items) {
|
||||||
return deep.mapValuesDeep(
|
return deepdash.mapValuesDeep(
|
||||||
items,
|
items,
|
||||||
(item) => {
|
(item) => {
|
||||||
return {
|
return {
|
||||||
@@ -172,15 +258,16 @@ function useAssocSidebarItemHasChildren(items) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the sub-sidebar post-menu.
|
* Retrieves the sub-sidebar post-menu.
|
||||||
* @param {*} submenuId
|
* @param {ISidebarMenuOverlayIds} submenuId
|
||||||
* @returns
|
* @returns {ISidebarMenuItem[]}
|
||||||
*/
|
*/
|
||||||
export function useSubSidebarMenu(submenuId) {
|
export function useSubSidebarMenu(submenuId) {
|
||||||
if (!submenuId) return [];
|
if (!submenuId) return [];
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
useBindSidebarItemLinkClick,
|
useBindSidebarItemClick,
|
||||||
useFlatSidebarMenu,
|
useFlatSidebarMenu,
|
||||||
|
filterSidebarItemHasNoChildren,
|
||||||
useFilterSidebarMenuAbility,
|
useFilterSidebarMenuAbility,
|
||||||
findSubmenuBySubmenuId(submenuId),
|
findSubmenuBySubmenuId(submenuId),
|
||||||
)(SidebarMenu);
|
)(SidebarMenu);
|
||||||
@@ -202,8 +289,22 @@ export function useObserveSidebarExpendedBodyclass(sidebarExpended) {
|
|||||||
*/
|
*/
|
||||||
export function useIsSidebarMenuItemActive(item) {
|
export function useIsSidebarMenuItemActive(item) {
|
||||||
const { submenuId } = useSidebarSubmenu();
|
const { submenuId } = useSidebarSubmenu();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
item.type === ISidebarMenuItemType.Overlay && submenuId === item.overlayId
|
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);
|
||||||
|
}
|
||||||
|
|||||||
33
src/utils/deep.js
Normal file
33
src/utils/deep.js
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,6 +14,8 @@ import { isEqual } from 'lodash';
|
|||||||
|
|
||||||
import jsCookie from 'js-cookie';
|
import jsCookie from 'js-cookie';
|
||||||
|
|
||||||
|
export * from './deep';
|
||||||
|
|
||||||
export const getCookie = (name, defaultValue) =>
|
export const getCookie = (name, defaultValue) =>
|
||||||
_.defaultTo(jsCookie.get(name), defaultValue);
|
_.defaultTo(jsCookie.get(name), defaultValue);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user