Compare commits

...

11 Commits

Author SHA1 Message Date
a.bouhuolia
4d00f53600 feat: release v1.7.6-rc.2 version. 2022-04-23 00:46:16 +02:00
a.bouhuolia
5e293e4f19 Merge branch 'develop' into main 2022-04-23 00:42:23 +02:00
Ahmed Bouhuolia
01038136f2 Update CHANGELOG.md 2022-04-23 00:41:18 +02:00
a.bouhuolia
6bc5eec8b6 Merge branch 'develop' into main 2022-04-23 00:38:28 +02:00
a.bouhuolia
1172e69d96 chore: add 1.7.4-rc.2 CHANGELOG. 2022-04-23 00:37:18 +02:00
a.bouhuolia
87758bf773 chore(Sidebar): docs. 2022-04-23 00:28:26 +02:00
a.bouhuolia
5cbb3c84e6 Merge branch 'BIG-374-refactoring-sidebar-menu-with-feature-and-permissions-control' into develop 2022-04-22 23:42:45 +02:00
a.bouhuolia
52924383bd feat(Sidebar): filter sidebar items based on subscription state. 2022-04-18 01:24:11 +02:00
a.bouhuolia
8d1825a065 feature(Sidebar): BIG-374 filtering the sidebar items based on each item feature support. 2022-04-18 00:16:37 +02:00
a.bouhuolia
5e4e9c37c3 feat(Sidebar): add the missing sidebar items. 2022-04-17 05:19:23 +02:00
a.bouhuolia
944bc29f4d feat(Sidebar): Refactoring sidebar menu with feature and permissions abilities control. 2022-04-17 05:05:35 +02:00
28 changed files with 1493 additions and 1069 deletions

View File

@@ -2,73 +2,95 @@
All notable changes to Bigcapital server-side will be in this file.
## [1.7.6-rc.2] - 23-04-2022
### Fixed
- `BIG-374` Refactoring sidebar men with ability permissions and feature control on each item.
## [v1.7.5-rc.2] - 20-04-2022
### Fixed.
- `BIG-378` Reports drawers columns css conflict.
## [1.7.3-rc.2] - 15-04-2022
### Fixed
- `BIG-372` Activate branches and warehouses dialog reloading once activating.
- `BIG-373` Issue general ledger report select specific account.
- `BIG-377` Make readonly details entries as oneline with tooltip for more details.
- `BIG-372` Activate branches and warehouses dialog reloading once activating.
- `BIG-373` Issue general ledger report select specific account.
- `BIG-377` Make readonly details entries as oneline with tooltip for more details.
## [1.7.2-rc.2] - 04-04-2022
### Fixed
- Add the missing Arabic localization.
- Subscription plans modifications.
- Add the missing Arabic localization.
- Subscription plans modifications.
## [1.7.1-rc.2] - 30-03-2022
## Added
- `BIG-141` Add inactive status to item drawer details.
- `BIG-278` Add created at date on expense details.
- `BIG-350` Add empty status content of warehouse transfers service.
- `BIG-344` Add branch details to manual journal and expense details.
- `BIG-141` Add inactive status to item drawer details.
- `BIG-278` Add created at date on expense details.
- `BIG-350` Add empty status content of warehouse transfers service.
- `BIG-344` Add branch details to manual journal and expense details.
## Fixed
- `BIG-221` Remove Non-inventory radio choice on item form.
- `BIG-236` Validate estimate expiration date should be equal or bigger than estimate date.
- `BIG-237` Validate invoice due date should be equal or bigger than invoice date.
- `BIG-238` Validate bill due date should be equal or bigger than bill date.
- `BIG-280` Optimize style of multi-select accounts menu.
- `BIG-284` Cashflow statement loading bar.
- `BIG-296` Creating a new child account from accounts list.
- `BIG-301` Navigation bar divider on actions bar hide with permissions control.
- `BIG-304` Adding cash or bank account from cash flow service.
- `BIG-351` Invalid date in the inventory adjustment detail.
- `BIG-352` Fix terms and notes fields on footer of all services.
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
- `BIG-221` Remove Non-inventory radio choice on item form.
- `BIG-236` Validate estimate expiration date should be equal or bigger than estimate date.
- `BIG-237` Validate invoice due date should be equal or bigger than invoice date.
- `BIG-238` Validate bill due date should be equal or bigger than bill date.
- `BIG-280` Optimize style of multi-select accounts menu.
- `BIG-284` Cashflow statement loading bar.
- `BIG-296` Creating a new child account from accounts list.
- `BIG-301` Navigation bar divider on actions bar hide with permissions control.
- `BIG-304` Adding cash or bank account from cash flow service.
- `BIG-351` Invalid date in the inventory adjustment detail.
- `BIG-352` Fix terms and notes fields on footer of all services.
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
## [1.7.0-rc.1] - 24-03-2022
## Added
- Multiply currencies with foreign currencies.
- Multiply warehouses to track inventory items.
- Multiply branches to track organization transactions.
- Transfer orders between warehouses.
- Integrate financial reports with multiply branches.
- Integrate inventory reports with multiply warehouses.
## Added
- Multiply currencies with foreign currencies.
- Multiply warehouses to track inventory items.
- Multiply branches to track organization transactions.
- Transfer orders between warehouses.
- Integrate financial reports with multiply branches.
- Integrate inventory reports with multiply warehouses.
## Changes
- Optimize style of sale invoice form.
- Optimize style of sale receipt form.
- Optimize style of credit note form.
- Optimize style of payment receive form.
- Optimize style of bill form.
- Optimize style of payment made form.
- Optimize style of manual journal form.
- Optimize style of expense form.
- Optimize style of sale invoice form.
- Optimize style of sale receipt form.
- Optimize style of credit note form.
- Optimize style of payment receive form.
- Optimize style of bill form.
- Optimize style of payment made form.
- Optimize style of manual journal form.
- Optimize style of expense form.
## [1.6.3] - 21-02-2022
### Fixed
- `BIG-337` Display billing page once the organization subscription is inactive.
- `BIG-337` Display billing page once the organization subscription is inactive.
## [1.6.2] - 19-02-2022
### Fixed
- fix syled components dependency with imported as default components.
- fix syled components dependency with imported as default components.
## [1.6.0] - 18-02-2022
### Added
- Balance sheet comparison of previous period (PP).
- Balance sheet comparison of previous year (PY).
- Balance sheet percentage analysis columns and rows basis.
@@ -79,10 +101,12 @@ All notable changes to Bigcapital server-side will be in this file.
## [1.5.8] - 13-01-2022
### Added
- Add payment receive PDF print.
- Add credit note PDF print.
### Fixed
- fix: Payment receive initial loading state depends on request loading state instead fetching.
- fix: Balance sheet report alert positioning.
- fix: Separate customer and vendor inactivate and activate alerts.

View File

@@ -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",

View File

@@ -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';

View File

@@ -104,7 +104,7 @@ import {
// textClassName?: string;
// }
export default class MenuItem extends AbstractPureComponent2 {
export class MenuItem extends AbstractPureComponent2 {
static get defaultProps() {
return {
disabled: false,

View File

@@ -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 <div class="bp3-menu-spacer" style={{ height: `${space}px` }} />;
}
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 (
<Choose>
<Choose.When condition={item.spacer}>
<SidebarMenuItemSpace space={item.spacer} />
</Choose.When>
<Choose.When condition={item.divider}>
<MenuDivider key={index} title={item.title} />
</Choose.When>
<Choose.When condition={item.label}>
<MenuItemLabel key={index} text={item.text} />
</Choose.When>
<Choose.Otherwise>
<MenuItem
key={index}
active={isActive}
icon={<Icon icon={item.icon} iconSize={item.iconSize} />}
text={item.text}
disabled={item.disabled}
dropdownType={item.dropdownType || 'collapse'}
caretIconSize={16}
onClick={handleItemClick}
callapseActive={!!isActive}
itemClassName={classNames({
'is-active': isActive,
'has-icon': !hasChildren && item.icon,
})}
hasSubmenu={hasChildren}
/>
</Choose.Otherwise>
</Choose>
);
});
};
const filterItems = menu.filter(
(item) => isSubscriptionActive || item.enableBilling,
);
const items = menuItemsMapper(filterItems);
const handleSidebarOverlayClose = () => {
setIsOpen(false);
};
return (
<div>
<Menu className="sidebar-menu">{items}</Menu>{' '}
<SidebarOverlay
isOpen={isOpen}
label={currentItem?.text || ''}
items={currentItem?.children || []}
onClose={handleSidebarOverlayClose}
/>
</div>
);
}
export default compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarMenu);

View File

@@ -1,48 +0,0 @@
import sidebarMenuList from 'config/sidebarMenu';
import { isArray, isEmpty } from 'lodash';
import { useAbilityContext } from 'hooks/utils';
export function useGetSidebarMenu() {
const ability = useAbilityContext();
return sidebarMenuList
.map((item) => {
const children = isArray(item.children)
? item.children.filter((childItem) => {
return isArray(childItem.permission)
? childItem.permission.some((perm) =>
ability.can(perm.ability, perm.subject),
)
: childItem?.permission?.ability && childItem?.permission?.subject
? ability.can(
childItem.permission.ability,
childItem.permission.subject,
)
: true;
})
: [];
return {
...item,
...(isArray(item.children)
? {
children,
}
: {}),
};
})
.filter((item) => {
return isArray(item.permission)
? item.permission.some((per) =>
ability.can(per.ability, per.subject),
)
: item?.permission?.ability && item?.permission?.subject
? ability.can(item.permission.ability, item.permission.subject)
: true;
})
.filter((item) =>
isEmpty(item.children) && !item.href && !item.label && !item.divider
? false
: true,
);
}

View File

@@ -1,146 +0,0 @@
import React from 'react';
import { Overlay } from '@blueprintjs/core';
import { Link } from 'react-router-dom';
import SidebarOverlayContainer from './SidebarOverlayContainer';
interface ISidebarOverlayItem {
text: string;
href: string;
divider?: boolean;
label?: boolean;
}
interface ISidebarOverlayProps {
isOpen: boolean;
items: ISidebarOverlayItem[];
label: string;
onClose: Function;
}
interface ISidebarOverlayItemProps {
text: string;
href: string;
onLinkClick: Function;
}
interface ISidebarOverlayItemDivider {
divider: boolean;
}
/**
* Sidebar overlay item.
*/
function SidebarOverlayItem({
text,
href,
onLinkClick,
}: ISidebarOverlayItemProps) {
const handleLinkClick = () => {
onLinkClick && onLinkClick();
};
return (
<div className="sidebar-overlay__item">
<Link onClick={handleLinkClick} to={href}>
{text}
</Link>
</div>
);
}
interface ISidebarOverlayItemLabel {
text: string;
}
function SidebarOverlayLabel({ text }: ISidebarOverlayItemLabel) {
return <div className="sidebar-overlay__label">{text}</div>;
}
function SidebarOverlayDivider() {
return <div className={'sidebar-overlay__divider'}></div>;
}
/**
* Sidebar overlay component.
*/
export default function SidebarOverlay({
label,
isOpen: controllerdIsOpen,
onClose,
items,
}: ISidebarOverlayProps) {
const [isEverOpened, setEverOpened] = React.useState(false);
const [isOpen, setIsOpen] = React.useState(controllerdIsOpen);
React.useEffect(() => {
if (
typeof controllerdIsOpen !== 'undefined' &&
isOpen !== controllerdIsOpen
) {
setIsOpen(controllerdIsOpen);
}
}, [controllerdIsOpen, setIsOpen, isOpen]);
React.useEffect(() => {
if (isOpen && !isEverOpened) {
setEverOpened(true);
}
}, [isEverOpened, isOpen]);
if (!isEverOpened) {
return '';
}
// Handle overlay close event.
const handleOverlayClose = () => {
setIsOpen(false);
onClose && onClose();
};
// Handle overlay open event.
const handleOverlayOpen = () => {
setIsOpen(true);
};
// Handle sidebar item link click.
const handleItemClick = () => {
setIsOpen(false);
onClose && onClose();
};
return (
<Overlay
isOpen={isOpen}
portalContainer={
(document.querySelector('.Pane.vertical.Pane2') as HTMLElement) ||
document.body
}
onClose={handleOverlayClose}
onOpening={handleOverlayOpen}
transitionDuration={100}
backdropClassName={'sidebar-overlay-backdrop'}
>
<div className="sidebar-overlay sidebar-overlay-transition">
<SidebarOverlayContainer>
<div className="sidebar-overlay__menu">
{label && (
<>
<SidebarOverlayLabel text={label} />
<SidebarOverlayDivider />
</>
)}
{items.map((item) =>
item.divider ? (
<SidebarOverlayDivider />
) : item.label ? (
<SidebarOverlayLabel text={item.text} />
) : (
<SidebarOverlayItem
onLinkClick={handleItemClick}
text={item.text}
href={item.href}
/>
),
)}
</div>
</SidebarOverlayContainer>
</div>
</Overlay>
);
}

View File

@@ -106,6 +106,7 @@ export * from './Paper';
export * from './Accounts';
export * from './DataTableCells';
export * from './FlexGrid';
export * from './MenuItem';
const Hint = FieldHint;

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,19 @@
import React from 'react';
import SidebarContainer from 'components/Sidebar/SidebarContainer';
import SidebarHead from 'components/Sidebar/SidebarHead';
import SidebarMenu from 'components/Sidebar/SidebarMenu';
import { useGetSidebarMenu } from './utils';
import { SidebarContainer } from './SidebarContainer';
import { SidebarHead } from './SidebarHead';
import { SidebarMenu } from './SidebarMenu';
import { useMainSidebarMenu } from './hooks';
import { SidebarOverlayBinded } from '../SidebarOverlay';
import 'style/containers/Dashboard/Sidebar.scss';
export default function Sidebar({ dashboardContentRef }) {
const menu = useGetSidebarMenu();
/**
* Dashboard sidebar.
* @returns {JSX.Element}
*/
export function Sidebar() {
const menu = useMainSidebarMenu();
return (
<SidebarContainer>
@@ -17,14 +22,14 @@ export default function Sidebar({ dashboardContentRef }) {
<div className="sidebar__menu">
<SidebarMenu menu={menu} />
</div>
<SidebarOverlayBinded />
<SidebarFooterVersion />
</SidebarContainer>
);
}
/**
* Sidebar footer version.
* Sidebar footer version.
* @returns {React.JSX}
*/
function SidebarFooterVersion() {

View File

@@ -1,19 +1,21 @@
import React, { useEffect } from 'react';
import { Scrollbar } from 'react-scrollbars-custom';
import classNames from 'classnames';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import { useObserveSidebarExpendedBodyclass } from './hooks';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function SidebarContainer({
/**
* Sidebar container/
* @returns {JSX.Element}
*/
function SidebarContainerJSX({
// #ownProps
children,
// #withDashboardActions
toggleSidebarExpend,
// #withDashboard
sidebarExpended,
@@ -22,9 +24,10 @@ function SidebarContainer({
}) {
const sidebarScrollerRef = React.useRef();
useEffect(() => {
document.body.classList.toggle('has-mini-sidebar', !sidebarExpended);
// Toggles classname to body once sidebar expend/shrink.
useObserveSidebarExpendedBodyclass(sidebarExpended);
useEffect(() => {
if (!sidebarExpended && sidebarScrollerRef.current) {
sidebarScrollerRef.current.scrollTo({
top: 0,
@@ -39,9 +42,9 @@ function SidebarContainer({
}
};
const scrollerElementRef = (ref) => {
const scrollerElementRef = React.useCallback((ref) => {
sidebarScrollerRef.current = ref;
};
}, []);
return (
<div
@@ -64,8 +67,7 @@ function SidebarContainer({
);
}
export default compose(
withDashboardActions,
export const SidebarContainer = compose(
withDashboard(({ sidebarExpended }) => ({
sidebarExpended,
})),
@@ -73,4 +75,4 @@ export default compose(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarContainer);
)(SidebarContainerJSX);

View File

@@ -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);

View File

@@ -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 (
<MenuItem
key={index}
text={item.text}
disabled={item.disabled}
dropdownType={item.dropdownType || 'collapse'}
caretIconSize={16}
onClick={item.onClick}
active={isActive}
hasSubmenu={item.hasChildren}
/>
);
}
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 ? (
<SidebarMenuItem item={item} index={index} />
) : // Group item type.
item.type === ISidebarMenuItemType.Group ? (
<MenuItemLabel text={item.text} />
) : null;
}
/**
* Sidebar menu.
* @returns {JSX.Element}
*/
function SidebarMenuJSX({ menu }) {
return (
<div>
<Menu className="sidebar-menu">
{menu.map((item, index) => (
<SidebarMenuItemComposer index={index} item={item} />
))}
</Menu>
</div>
);
}
export const SidebarMenu = R.compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarMenuJSX);

View File

@@ -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);
}

View File

@@ -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',
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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 (
<div className="sidebar-overlay__item">
<Link to={href} onClick={onClick}>
{text}
</Link>
</div>
);
}
export interface ISidebarOverlayItemLabel {
text: string;
}
/**
* Sidebar overlay label item.
* @param {ISidebarOverlayItemLabel}
* @returns {JSX.Element}
*/
export function SidebarOverlayLabel({
text,
}: ISidebarOverlayItemLabel): JSX.Element {
return <div className="sidebar-overlay__label">{text}</div>;
}
/**
* Sidebar overlay divider item.
* @returns {JSX.Element}
*/
export function SidebarOverlayDivider() {
return <div className={'sidebar-overlay__divider'}></div>;
}
interface SidebarOverlayItemProps {
item: ISidebarMenuItem;
}
/**
* Sidebar overlay item.
* @param {SidebarOverlayItemProps} props -
* @returns {JSX.Element}
*/
function SidebarOverlayItem({ item }: SidebarOverlayItemProps) {
//
return item.type === ISidebarMenuItemType.Group ? (
<SidebarOverlayLabel text={item.text} />
) : //
item.type === ISidebarMenuItemType.Link ||
item.type === ISidebarMenuItemType.Dialog ? (
<SidebarOverlayItemLink text={item.text} onClick={item.onClick} />
) : null;
}
/**
*
*/
export interface ISidebarOverlayMenu {
items: ISidebarMenuItem[];
}
/**
* Sidebar overlay menu.
* @param {ISidebarOverlayMenu}
* @returns {JSX.Element}
*/
function SidebarOverlayMenu({ items }: ISidebarOverlayMenu) {
return (
<div className="sidebar-overlay__menu">
{items.map((item) => (
<SidebarOverlayItem item={item} />
))}
</div>
);
}
export interface SidebarOverlayProps extends OverlayProps {
items: ISidebarMenuItem[];
}
/**
* Sidebar overlay component.
* @param {SidebarOverlayProps}
* @returns {JSX.Element}
*/
export function SidebarOverlay({ items, label, ...rest }: SidebarOverlayProps) {
return (
<Overlay
portalContainer={
(document.querySelector('.Pane.vertical.Pane2') as HTMLElement) ||
document.body
}
transitionDuration={100}
backdropClassName={'sidebar-overlay-backdrop'}
{...rest}
>
<div className="sidebar-overlay sidebar-overlay-transition">
<SidebarOverlayContainer>
{label && <SidebarOverlayLabel text={label} />}
<SidebarOverlayMenu items={items} />
</SidebarOverlayContainer>
</div>
</Overlay>
);
}

View File

@@ -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 (
<SidebarOverlayBindedRouter
sidebarSubmenuId={sidebarSubmenuId}
isOpen={sidebarSubmenuOpen}
onClose={handleSidebarClosing}
/>
);
}
/**
* Dashboard sidebar submenu router.
*/
function SidebarOverlayBindedRouter({ sidebarSubmenuId, ...rest }) {
const sidebarItems = useSubSidebarMenu(sidebarSubmenuId);
return <SidebarOverlay items={sidebarItems} {...rest} />;
}
/**
* Sidebar overlay binded with redux.
*/
export const SidebarOverlayBinded = R.compose(
withDashboardSidebar(({ sidebarSubmenuOpen, sidebarSubmenuId }) => ({
sidebarSubmenuOpen,
sidebarSubmenuId,
})),
withDashboardSidebarActions,
)(SidebarOverlayBindedRoot);

View File

@@ -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 (
<div className={'sidebar-overlay__scroll-wrapper'}>
<Scrollbar noDefaultStyles={true}>

View File

@@ -0,0 +1,3 @@
export * from './SidebarOverlay';
export * from './SidebarOverlayContainer';
export * from './SidebarOverlayBinded'

View File

@@ -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),
};
};

View File

@@ -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,
};
}

View File

@@ -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);
},

View File

@@ -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',

View File

@@ -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;

33
src/utils/deep.js Normal file
View 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,
},
),
);
};

View File

@@ -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);