diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65b237dec..252112bcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to Bigcapital server-side will be in this file.
+## [1.7.4-rc.2] - 20-04-2022
+ - `BIG-374` Refactoring sidebar men with ability permissions and feature control on each item.
+
+### Fixed
+ -
## [1.7.3-rc.2] - 15-04-2022
### Fixed
diff --git a/package.json b/package.json
index cf209c2af..a8ee35565 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"cross-env": "^7.0.2",
"css-loader": "3.4.2",
"deep-map-keys": "^2.0.1",
+ "deepdash": "^5.3.9",
"dependency-graph": "^0.11.0",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
diff --git a/src/components/Dashboard/Dashboard.js b/src/components/Dashboard/Dashboard.js
index dbfd47c4e..9933ef858 100644
--- a/src/components/Dashboard/Dashboard.js
+++ b/src/components/Dashboard/Dashboard.js
@@ -3,7 +3,7 @@ import { Switch, Route } from 'react-router';
import 'style/pages/Dashboard/Dashboard.scss';
-import Sidebar from 'components/Sidebar/Sidebar';
+import { Sidebar } from 'containers/Dashboard/Sidebar/Sidebar';
import DashboardContent from 'components/Dashboard/DashboardContent';
import DialogsContainer from 'components/DialogsContainer';
import PreferencesPage from 'components/Preferences/PreferencesPage';
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index 74144e45b..e03fc74ad 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -104,7 +104,7 @@ import {
// textClassName?: string;
// }
-export default class MenuItem extends AbstractPureComponent2 {
+export class MenuItem extends AbstractPureComponent2 {
static get defaultProps() {
return {
disabled: false,
diff --git a/src/components/Sidebar/SidebarMenu.js b/src/components/Sidebar/SidebarMenu.js
deleted file mode 100644
index 5a8d2bd82..000000000
--- a/src/components/Sidebar/SidebarMenu.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import React from 'react';
-import { Menu, MenuDivider } from '@blueprintjs/core';
-import { useHistory, useLocation } from 'react-router-dom';
-
-import { Choose } from 'components';
-import Icon from 'components/Icon';
-import MenuItem from 'components/MenuItem';
-import { MenuItemLabel } from 'components';
-import classNames from 'classnames';
-import SidebarOverlay from 'components/SidebarOverlay';
-import { compose } from 'redux';
-import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
-
-const DEFAULT_ITEM = {
- text: '',
- href: '',
-};
-
-function matchPath(pathname, path, matchExact) {
- return matchExact ? pathname === path : pathname.indexOf(path) !== -1;
-}
-
-function SidebarMenuItemSpace({ space }) {
- return
;
-}
-
-function SidebarMenu({ menu, isSubscriptionActive }) {
- const history = useHistory();
- const location = useLocation();
-
- const [isOpen, setIsOpen] = React.useState(false);
- const [currentItem, setCurrentItem] = React.useState(null);
-
- const menuItemsMapper = (list) => {
- return list.map((item, index) => {
- const hasChildren = Array.isArray(item.children);
-
- const isActive =
- (item.children
- ? item.children.some((c) =>
- matchPath(location.pathname, c.href, item.matchExact),
- )
- : item.href &&
- matchPath(location.pathname, item.href, item.matchExact)) ||
- currentItem === item;
-
- const handleItemClick = () => {
- if (item.href) {
- history.push(item.href);
- }
- if (item.children && item.children.length > 0) {
- setIsOpen(true);
- setCurrentItem(item);
- } else {
- setIsOpen(false);
- }
- };
-
- return (
- ({
sidebarExpended,
})),
@@ -73,4 +75,4 @@ export default compose(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
-)(SidebarContainer);
+)(SidebarContainerJSX);
diff --git a/src/components/Sidebar/SidebarHead.js b/src/containers/Dashboard/Sidebar/SidebarHead.js
similarity index 86%
rename from src/components/Sidebar/SidebarHead.js
rename to src/containers/Dashboard/Sidebar/SidebarHead.js
index 25f6d390f..cac2b3aa0 100644
--- a/src/components/Sidebar/SidebarHead.js
+++ b/src/containers/Dashboard/Sidebar/SidebarHead.js
@@ -1,9 +1,11 @@
import React from 'react';
import { Button, Popover, Menu, Position } from '@blueprintjs/core';
-import Icon from 'components/Icon';
+
+import { Icon } from 'components';
+
+import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
+import { useAuthenticatedAccount } from 'hooks/query';
import { compose, firstLettersArgs } from 'utils';
-import withCurrentOrganization from '../../containers/Organization/withCurrentOrganization';
-import { useAuthenticatedAccount } from '../../hooks/query';
// Popover modifiers.
const POPOVER_MODIFIERS = {
@@ -13,7 +15,7 @@ const POPOVER_MODIFIERS = {
/**
* Sideabr head.
*/
-function SidebarHead({
+function SidebarHeadJSX({
// #withCurrentOrganization
organization,
}) {
@@ -61,6 +63,6 @@ function SidebarHead({
);
}
-export default compose(
+export const SidebarHead = compose(
withCurrentOrganization(({ organization }) => ({ organization })),
-)(SidebarHead);
+)(SidebarHeadJSX);
diff --git a/src/containers/Dashboard/Sidebar/SidebarMenu.js b/src/containers/Dashboard/Sidebar/SidebarMenu.js
new file mode 100644
index 000000000..82572a755
--- /dev/null
+++ b/src/containers/Dashboard/Sidebar/SidebarMenu.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Menu } from '@blueprintjs/core';
+import * as R from 'ramda';
+
+import { MenuItem, MenuItemLabel } from 'components';
+import { ISidebarMenuItemType } from 'containers/Dashboard/Sidebar/interfaces';
+import { useIsSidebarMenuItemActive } from './hooks';
+
+import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
+
+/**
+ * Sidebar menu item.
+ * @returns {JSX.Element}
+ */
+function SidebarMenuItem({ item, index }) {
+ // Detarmine whether the item is active.
+ const isActive = useIsSidebarMenuItemActive(item);
+
+ return (
+
+ );
+}
+
+SidebarMenuItem.ItemTypes = [
+ ISidebarMenuItemType.Link,
+ ISidebarMenuItemType.Overlay,
+ ISidebarMenuItemType.Dialog,
+];
+
+/**
+ * Detarmines which sidebar menu item type should display.
+ * @returns {JSX.Element}
+ */
+function SidebarMenuItemComposer({ item, index }) {
+ // Link item type.
+ return SidebarMenuItem.ItemTypes.indexOf(item.type) !== -1 ? (
+
+ ) : // Group item type.
+ item.type === ISidebarMenuItemType.Group ? (
+
+ ) : null;
+}
+
+/**
+ * Sidebar menu.
+ * @returns {JSX.Element}
+ */
+function SidebarMenuJSX({ menu }) {
+ return (
+
+
+
+ );
+}
+
+export const SidebarMenu = R.compose(
+ withSubscriptions(
+ ({ isSubscriptionActive }) => ({ isSubscriptionActive }),
+ 'main',
+ ),
+)(SidebarMenuJSX);
diff --git a/src/containers/Dashboard/Sidebar/hooks.js b/src/containers/Dashboard/Sidebar/hooks.js
new file mode 100644
index 000000000..df8168d13
--- /dev/null
+++ b/src/containers/Dashboard/Sidebar/hooks.js
@@ -0,0 +1,358 @@
+import _, { isEmpty, includes } from 'lodash';
+import React from 'react';
+import * as R from 'ramda';
+import { useHistory } from 'react-router-dom';
+
+import { useAbilityContext } from 'hooks';
+import { useSidebarSubmenu, useFeatureCan } from 'hooks/state';
+import { SidebarMenu } from 'config/sidebarMenu';
+import {
+ ISidebarMenuItemType,
+ ISidebarSubscriptionAbility,
+} from './interfaces';
+import {
+ useSidebarSubmnuActions,
+ useDialogActions,
+ useSubscription,
+} from 'hooks/state';
+import { filterValuesDeep, deepdash } from 'utils';
+
+const deepDashConfig = {
+ childrenPath: 'children',
+ pathFormat: 'array',
+};
+const ingoreTypesEmpty = [
+ ISidebarMenuItemType.Group,
+ ISidebarMenuItemType.Overlay,
+];
+
+/**
+ * Removes the all overlay items from the menu to the main-sidebar.
+ * @param {ISidebarMenuItem[]} menu
+ * @returns {ISidebarMenuItem[]}
+ */
+function removeSidebarOverlayChildren(menu) {
+ return deepdash.mapValuesDeep(
+ menu,
+ (item, key, parent, context) => {
+ if (item.type === ISidebarMenuItemType.Overlay) {
+ context.skipChildren(true);
+ return _.omit(item, ['children']);
+ }
+ return item;
+ },
+ deepDashConfig,
+ );
+}
+
+/**
+ * Retrives the main sidebar pre-menu.
+ * @returns {ISidebarMenuItem[]}
+ */
+export function getMainSidebarMenu() {
+ return R.compose(removeSidebarOverlayChildren)(SidebarMenu);
+}
+
+/**
+ * Predicates whether the sidebar item has feature ability.
+ */
+function useFilterSidebarItemFeaturePredicater() {
+ const { featureCan } = useFeatureCan();
+
+ return {
+ // Returns false if the item has `feature` prop and that feature has no ability.
+ predicate: (item) => {
+ if (item.feature && !featureCan(item.feature)) {
+ return false;
+ }
+ return true;
+ },
+ };
+}
+
+/**
+ * Predicates whether the sidebar item has permissio ability.
+ */
+function useFilterSidebarItemAbilityPredicater() {
+ const ability = useAbilityContext();
+
+ return {
+ // Retruns false if the item has `permission` prop and that permission has no ability.
+ predicate: (item) => {
+ if (
+ item.permission &&
+ !ability.can(item.permission.ability, item.permission.subject)
+ ) {
+ return false;
+ }
+ return true;
+ },
+ };
+}
+
+/**
+ * Filters the sidebar item based on the subscription state.
+ */
+function useFilterSidebarItemSubscriptionPredicater() {
+ const { isSubscriptionActive, isSubscriptionInactive } = useSubscription();
+
+ return {
+ predicate: (item) => {
+ const { subscription } = item;
+
+ if (subscription) {
+ const isActive = includes(subscription, [
+ ISidebarSubscriptionAbility.Active,
+ ])
+ ? isSubscriptionActive
+ : true;
+
+ const isInactive = includes(subscription, [
+ ISidebarSubscriptionAbility.Inactive,
+ ])
+ ? isSubscriptionInactive
+ : true;
+
+ return isActive && isInactive;
+ }
+ 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();
+ const { predicate: predSubscription } =
+ useFilterSidebarItemSubscriptionPredicater();
+
+ return deepdash.filterDeep(
+ menu,
+ (item) => {
+ return predFeature(item) && predAbility(item) && predSubscription(item);
+ },
+ deepDashConfig,
+ );
+}
+
+/**
+ * Flats the sidebar menu groups.
+ * @param {*} menu
+ * @returns {}
+ */
+function useFlatSidebarMenu(menu) {
+ return React.useMemo(() => {
+ return deepdash.mapDeep(menu, (item) => item, deepDashConfig);
+ }, [menu]);
+}
+
+/**
+ * Binds sidebar link item click action.
+ * @param {ISidebarMenuItem} item
+ */
+function useBindSidebarItemLinkClick() {
+ const history = useHistory();
+ 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 onClick = (item) => (event) => {
+ closeSidebarSubmenu();
+ openDialog(item.dialogName, item.dialogPayload);
+ };
+ return {
+ bindOnClick: (item) => {
+ return {
+ ...item,
+ onClick: onClick(item),
+ };
+ },
+ };
+}
+
+/**
+ * Binds click action for the sidebar overlay 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),
+ };
+ },
+ };
+}
+
+/**
+ * Binds click action of the given sidebar menu for each item based on item type.
+ */
+function useBindSidebarItemClick(menu) {
+ const { bindOnClick: bindLinkClickEvt } = useBindSidebarItemLinkClick();
+ const { bindOnClick: bindOverlayClickEvt } = useBindSidebarItemOverlayClick();
+ const { bindOnClick: bindItemDialogEvt } = useBindSidebarItemDialogClick();
+
+ return React.useMemo(() => {
+ return deepdash.mapValuesDeep(
+ menu,
+ (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,
+ );
+ }, [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 = deepdash.findDeep(
+ menu,
+ (item) => {
+ return (
+ item.type === ISidebarMenuItemType.Overlay &&
+ item.overlayId === submenuId
+ );
+ },
+ deepDashConfig,
+ );
+ return groupItem?.value?.children || [];
+});
+
+/**
+ * Retrieves the main sidebar post-menu.
+ * @returns {ISidebarMenuItem[]}
+ */
+export function useMainSidebarMenu() {
+ return R.compose(
+ useBindSidebarItemClick,
+ useFlatSidebarMenu,
+ removeSidebarOverlayChildren,
+ useAssocSidebarItemHasChildren,
+ filterSidebarItemHasNoChildren,
+ useFilterSidebarMenuAbility,
+ )(SidebarMenu);
+}
+
+/**
+ * Assoc `hasChildren` prop to sidebar menu items.
+ * @param {ISidebarMenuItem[]} items
+ * @returns {ISidebarMenuItem[]}
+ */
+function useAssocSidebarItemHasChildren(items) {
+ return deepdash.mapValuesDeep(
+ items,
+ (item) => {
+ return {
+ ...item,
+ hasChildren: !isEmpty(item.children),
+ };
+ },
+ deepDashConfig,
+ );
+}
+
+/**
+ * Retrieves the sub-sidebar post-menu.
+ * @param {ISidebarMenuOverlayIds} submenuId
+ * @returns {ISidebarMenuItem[]}
+ */
+export function useSubSidebarMenu(submenuId) {
+ if (!submenuId) return [];
+
+ return R.compose(
+ useBindSidebarItemClick,
+ useFlatSidebarMenu,
+ filterSidebarItemHasNoChildren,
+ useFilterSidebarMenuAbility,
+ findSubmenuBySubmenuId(submenuId),
+ )(SidebarMenu);
+}
+
+/**
+ * Observes the sidebar expending with body class.
+ * @param {boolean} sidebarExpended
+ */
+export function useObserveSidebarExpendedBodyclass(sidebarExpended) {
+ React.useEffect(() => {
+ document.body.classList.toggle('has-mini-sidebar', !sidebarExpended);
+ }, [sidebarExpended]);
+}
+
+/**
+ * Detamrines whether the given sidebar menu item is active.
+ * @returns {boolean}
+ */
+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/config/interfaces.tsx b/src/containers/Dashboard/Sidebar/interfaces.ts
similarity index 56%
rename from src/config/interfaces.tsx
rename to src/containers/Dashboard/Sidebar/interfaces.ts
index db265d6f4..b3f7eaa3a 100644
--- a/src/config/interfaces.tsx
+++ b/src/containers/Dashboard/Sidebar/interfaces.ts
@@ -2,7 +2,9 @@ export enum ISidebarMenuItemType {
Label = 'label',
Link = 'link',
Group = 'group',
- Overlay = 'overlay'
+ Overlay = 'overlay',
+ Dialog = 'dialog',
+ Drawer = 'drawer',
}
export interface ISidebarMenuItemOverlay extends ISidebarMenuItemCommon {
@@ -16,6 +18,18 @@ export interface ISidebarMenuItemLink extends ISidebarMenuItemCommon {
matchExact?: boolean;
}
+export interface ISidebarMenuItemDialog extends ISidebarMenuItemCommon {
+ type: ISidebarMenuItemType.Dialog;
+ dialogName: string;
+ dialogPayload: any;
+}
+
+export interface ISidebarMenuItemDrawer extends ISidebarMenuItemCommon {
+ type: ISidebarMenuItemType.Drawer;
+ drawerName: string;
+ drawerPayload: any;
+}
+
export interface ISidebarMenuItemLabel extends ISidebarMenuItemCommon {
text?: string;
type: ISidebarMenuItemType.Label;
@@ -42,4 +56,22 @@ export type ISidebarMenuItem =
| ISidebarMenuItemLink
| ISidebarMenuItemLabel
| ISidebarMenuItemGroup
- | ISidebarMenuItemOverlay;
+ | ISidebarMenuItemOverlay
+ | ISidebarMenuItemDialog
+ | ISidebarMenuItemDrawer;
+
+export enum ISidebarMenuOverlayIds {
+ Items = 'Items',
+ Reports = 'Reports',
+ Sales = 'Sales',
+ Purchases = 'Purchases',
+ Financial = 'Financial',
+ Contacts = 'Contacts',
+ Cashflow = 'Cashflow',
+ Expenses = 'Expenses',
+}
+
+export enum ISidebarSubscriptionAbility {
+ Expired = 'SubscriptionExpired',
+ Active = 'SubscriptionActive',
+}
diff --git a/src/containers/Dashboard/Sidebar/withDashboardSidebar.js b/src/containers/Dashboard/Sidebar/withDashboardSidebar.js
new file mode 100644
index 000000000..c775a164e
--- /dev/null
+++ b/src/containers/Dashboard/Sidebar/withDashboardSidebar.js
@@ -0,0 +1,13 @@
+
+import { connect } from 'react-redux';
+
+export default (mapState) => {
+ const mapStateToProps = (state, props) => {
+ const mapped = {
+ sidebarSubmenuOpen: state.dashboard.sidebarSubmenu.isOpen,
+ sidebarSubmenuId: state.dashboard.sidebarSubmenu.submenuId,
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+}
diff --git a/src/containers/Dashboard/Sidebar/withDashboardSidebarActions.js b/src/containers/Dashboard/Sidebar/withDashboardSidebarActions.js
new file mode 100644
index 000000000..75083265e
--- /dev/null
+++ b/src/containers/Dashboard/Sidebar/withDashboardSidebarActions.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+import {
+ closeSidebarSubmenu,
+ openSidebarSubmenu,
+} from 'store/dashboard/dashboard.actions';
+
+const mapActionsToProps = (dispatch) => ({
+ // Opens the dashboard submenu sidebar.
+ openDashboardSidebarSubmenu: (submenuId) =>
+ dispatch(openSidebarSubmenu(submenuId)),
+
+ // Closes the dashboard submenu sidebar.
+ closeDashboardSidebarSubmenu: () => dispatch(closeSidebarSubmenu()),
+});
+
+export default connect(null, mapActionsToProps);
diff --git a/src/containers/Dashboard/SidebarOverlay/SidebarOverlay.tsx b/src/containers/Dashboard/SidebarOverlay/SidebarOverlay.tsx
new file mode 100644
index 000000000..dc14e63f9
--- /dev/null
+++ b/src/containers/Dashboard/SidebarOverlay/SidebarOverlay.tsx
@@ -0,0 +1,142 @@
+//@ts-nocheck
+import React from 'react';
+import { Overlay, OverlayProps } from '@blueprintjs/core';
+import { Link } from 'react-router-dom';
+import { SidebarOverlayContainer } from './SidebarOverlayContainer';
+import { ISidebarMenuItem, ISidebarMenuItemType } from '../Sidebar/interfaces';
+
+export interface ISidebarOverlayItem {
+ text: string;
+ href: string;
+ divider?: boolean;
+ label?: boolean;
+}
+
+export interface ISidebarOverlayProps {
+ items: ISidebarMenuItem[];
+}
+
+export interface ISidebarOverlayItemProps {
+ text: string | JSX.Element;
+ href?: string;
+ onClick?: any;
+}
+
+export interface ISidebarOverlayItemDivider {
+ divider: boolean;
+}
+
+/**
+ * Sidebar overlay item.
+ * @param {ISidebarOverlayItemProps}
+ * @returns {JSX.Element}
+ */
+export function SidebarOverlayItemLink({
+ text,
+ href,
+ onClick,
+}: ISidebarOverlayItemProps) {
+ return (
+
+
+ {text}
+
+
+ );
+}
+
+export interface ISidebarOverlayItemLabel {
+ text: string;
+}
+
+/**
+ * Sidebar overlay label item.
+ * @param {ISidebarOverlayItemLabel}
+ * @returns {JSX.Element}
+ */
+export function SidebarOverlayLabel({
+ text,
+}: ISidebarOverlayItemLabel): JSX.Element {
+ return
{text}
;
+}
+
+/**
+ * Sidebar overlay divider item.
+ * @returns {JSX.Element}
+ */
+export function SidebarOverlayDivider() {
+ return
;
+}
+
+interface SidebarOverlayItemProps {
+ item: ISidebarMenuItem;
+}
+
+/**
+ * Sidebar overlay item.
+ * @param {SidebarOverlayItemProps} props -
+ * @returns {JSX.Element}
+ */
+function SidebarOverlayItem({ item }: SidebarOverlayItemProps) {
+ //
+ return item.type === ISidebarMenuItemType.Group ? (
+
+ ) : //
+ item.type === ISidebarMenuItemType.Link ||
+ item.type === ISidebarMenuItemType.Dialog ? (
+
+ ) : null;
+}
+
+/**
+ *
+ */
+export interface ISidebarOverlayMenu {
+ items: ISidebarMenuItem[];
+}
+
+/**
+ * Sidebar overlay menu.
+ * @param {ISidebarOverlayMenu}
+ * @returns {JSX.Element}
+ */
+function SidebarOverlayMenu({ items }: ISidebarOverlayMenu) {
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+}
+
+export interface SidebarOverlayProps extends OverlayProps {
+ items: ISidebarMenuItem[];
+}
+
+/**
+ * Sidebar overlay component.
+ * @param {SidebarOverlayProps}
+ * @returns {JSX.Element}
+ */
+export function SidebarOverlay({ items, label, ...rest }: SidebarOverlayProps) {
+ return (
+
+
+
+ {label && }
+
+
+
+
+
+ );
+}
diff --git a/src/containers/Dashboard/SidebarOverlay/SidebarOverlayBinded.tsx b/src/containers/Dashboard/SidebarOverlay/SidebarOverlayBinded.tsx
new file mode 100644
index 000000000..ca8eb4d59
--- /dev/null
+++ b/src/containers/Dashboard/SidebarOverlay/SidebarOverlayBinded.tsx
@@ -0,0 +1,54 @@
+// @ts-nocheck
+import React from 'react';
+import * as R from 'ramda';
+
+import { SidebarOverlay } from './SidebarOverlay';
+import withDashboardSidebarActions from 'containers/Dashboard/Sidebar/withDashboardSidebarActions';
+import withDashboardSidebar from 'containers/Dashboard/Sidebar/withDashboardSidebar';
+
+import { useSubSidebarMenu } from '../Sidebar/hooks';
+
+/**
+ * Dashboard sidebar menu.
+ * @returns {JSX.Element}
+ */
+function SidebarOverlayBindedRoot({
+ // #withDashboardSidebar
+ sidebarSubmenuOpen,
+ sidebarSubmenuId,
+
+ // #withDashboardSidebarActions
+ closeDashboardSidebarSubmenu,
+}) {
+ const handleSidebarClosing = React.useCallback(() => {
+ closeDashboardSidebarSubmenu();
+ }, []);
+
+ return (
+
+ );
+}
+
+/**
+ * Dashboard sidebar submenu router.
+ */
+function SidebarOverlayBindedRouter({ sidebarSubmenuId, ...rest }) {
+ const sidebarItems = useSubSidebarMenu(sidebarSubmenuId);
+
+ return
;
+}
+
+/**
+ * Sidebar overlay binded with redux.
+ */
+export const SidebarOverlayBinded = R.compose(
+ withDashboardSidebar(({ sidebarSubmenuOpen, sidebarSubmenuId }) => ({
+ sidebarSubmenuOpen,
+ sidebarSubmenuId,
+ })),
+ withDashboardSidebarActions,
+)(SidebarOverlayBindedRoot);
diff --git a/src/components/SidebarOverlay/SidebarOverlayContainer.tsx b/src/containers/Dashboard/SidebarOverlay/SidebarOverlayContainer.tsx
similarity index 64%
rename from src/components/SidebarOverlay/SidebarOverlayContainer.tsx
rename to src/containers/Dashboard/SidebarOverlay/SidebarOverlayContainer.tsx
index 0122127e1..c44c88d7b 100644
--- a/src/components/SidebarOverlay/SidebarOverlayContainer.tsx
+++ b/src/containers/Dashboard/SidebarOverlay/SidebarOverlayContainer.tsx
@@ -1,14 +1,16 @@
import React from 'react';
import { Scrollbar } from 'react-scrollbars-custom';
-interface ISidebarOverlayContainerProps {
- children: JSX.Element | JSX.Element[],
+export interface ISidebarOverlayContainerProps {
+ children: JSX.Element | JSX.Element[];
}
/**
* Sidebar overlay container.
*/
-export default function SidebarOverlayContainer({ children }: ISidebarOverlayContainerProps) {
+export function SidebarOverlayContainer({
+ children,
+}: ISidebarOverlayContainerProps) {
return (
diff --git a/src/containers/Dashboard/SidebarOverlay/index.ts b/src/containers/Dashboard/SidebarOverlay/index.ts
new file mode 100644
index 000000000..7900f6d30
--- /dev/null
+++ b/src/containers/Dashboard/SidebarOverlay/index.ts
@@ -0,0 +1,3 @@
+export * from './SidebarOverlay';
+export * from './SidebarOverlayContainer';
+export * from './SidebarOverlayBinded'
\ No newline at end of file
diff --git a/src/hooks/state/dashboard.js b/src/hooks/state/dashboard.js
index 04c22e37f..0e1846ecd 100644
--- a/src/hooks/state/dashboard.js
+++ b/src/hooks/state/dashboard.js
@@ -1,9 +1,14 @@
import { useCallback } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
import {
splashStopLoading,
splashStartLoading,
dashboardPageTitle,
+ openSidebarSubmenu,
+ closeSidebarSubmenu,
+ openDialog,
+ closeDialog,
} from '../../store/dashboard/dashboard.actions';
export const useDispatchAction = (action) => {
@@ -30,3 +35,44 @@ export const useSplashLoading = () => {
useDispatchAction(splashStopLoading),
];
};
+
+/**
+ * Sidebar submenu actions.
+ */
+export const useSidebarSubmnuActions = () => {
+ return {
+ openSidebarSubmenu: useDispatchAction(openSidebarSubmenu),
+ closeSidebarSubmenu: useDispatchAction(closeSidebarSubmenu),
+ toggleSidebarSubmenu: useDispatchAction(openSidebarSubmenu),
+ };
+};
+
+/**
+ * Retrieves the sidebar submenu selector.
+ */
+const sidebarSubmenuSelector = createSelector(
+ (state) => state.dashboard.sidebarSubmenu,
+ (sidebarSubmenu) => sidebarSubmenu,
+);
+
+/**
+ * Retrieves the sidebar submenu selector.
+ */
+export const useSidebarSubmenu = () => {
+ const sidebarSubmenu = useSelector(sidebarSubmenuSelector);
+
+ return {
+ isOpen: sidebarSubmenu?.isOpen || false,
+ submenuId: sidebarSubmenu?.submenuId || null,
+ };
+};
+
+/**
+ * Dialogs actions.
+ */
+export const useDialogActions = () => {
+ return {
+ openDialog: useDispatchAction(openDialog),
+ closeDialog: useDispatchAction(closeDialog),
+ };
+};
diff --git a/src/store/dashboard/dashboard.actions.js b/src/store/dashboard/dashboard.actions.js
index 5d6222faa..6a257a15c 100644
--- a/src/store/dashboard/dashboard.actions.js
+++ b/src/store/dashboard/dashboard.actions.js
@@ -111,3 +111,16 @@ export const setFeatureDashboardMeta = ({ features }) => {
},
};
};
+
+export function openSidebarSubmenu({ submenuId }) {
+ return {
+ type: t.SIDEBAR_SUBMENU_OPEN,
+ payload: { submenuId },
+ };
+}
+
+export function closeSidebarSubmenu() {
+ return {
+ type: t.SIDEBAR_SUBMENU_CLOSE,
+ };
+}
diff --git a/src/store/dashboard/dashboard.reducer.js b/src/store/dashboard/dashboard.reducer.js
index 424bf9e0c..0bf38a06c 100644
--- a/src/store/dashboard/dashboard.reducer.js
+++ b/src/store/dashboard/dashboard.reducer.js
@@ -20,10 +20,8 @@ const initialState = {
splashScreenLoading: null,
appIsLoading: true,
appIntlIsLoading: true,
- features: {
- // branches: true,
- // warehouses: true,
- },
+ sidebarSubmenu: { isOpen: false, submenuId: null },
+ features: {},
};
const STORAGE_KEY = 'bigcapital:dashboard';
@@ -131,6 +129,16 @@ const reducerInstance = createReducer(initialState, {
state.splashScreenLoading = Math.max(state.splashScreenLoading, 0);
},
+ [t.SIDEBAR_SUBMENU_OPEN]: (state, action) => {
+ state.sidebarSubmenu.isOpen = true;
+ state.sidebarSubmenu.submenuId = action.payload.submenuId;
+ },
+
+ [t.SIDEBAR_SUBMENU_CLOSE]: (state, action) => {
+ state.sidebarSubmenu.isOpen = false;
+ state.sidebarSubmenu.submenuId = null;
+ },
+
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
diff --git a/src/store/dashboard/dashboard.types.js b/src/store/dashboard/dashboard.types.js
index 7374a0437..d613b94ab 100644
--- a/src/store/dashboard/dashboard.types.js
+++ b/src/store/dashboard/dashboard.types.js
@@ -13,6 +13,9 @@ export default {
ALTER_DASHBOARD_PAGE_SUBTITLE: 'ALTER_DASHBOARD_PAGE_SUBTITLE',
SET_TOPBAR_EDIT_VIEW: 'SET_TOPBAR_EDIT_VIEW',
SIDEBAR_EXPEND_TOGGLE: 'SIDEBAR_EXPEND_TOGGLE',
+ SIDEBAR_SUBMENU_OPEN: 'SIDEBAR_SUBMENU_OPEN',
+ SIDEBAR_SUBMENU_CLOSE: 'SIDEBAR_SUBMENU_CLOSE',
+ SIDEBAR_SUBMENU_TOGGLE: 'SIDEBAR_SUBMENU_TOGGLE',
SET_DASHBOARD_BACK_LINK: 'SET_DASHBOARD_BACK_LINK',
SPLASH_START_LOADING: 'SPLASH_START_LOADING',
SPLASH_STOP_LOADING: 'SPLASH_STOP_LOADING',
diff --git a/src/style/components/SidebarOverlay.scss b/src/style/components/SidebarOverlay.scss
index ad4bb1ecc..c6f83b92c 100644
--- a/src/style/components/SidebarOverlay.scss
+++ b/src/style/components/SidebarOverlay.scss
@@ -75,6 +75,8 @@
padding: 14px 20px 10px;
letter-spacing: 1px;
color: #707a85;
+ border-bottom: 1px solid #e2e5ec;
+ margin-bottom: 6px;
html[lang^="ar"] & {
font-size: 13px;
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);