init: add fuse-react v8.3.5 skeleton
This commit is contained in:
19
src/@fuse/colors/fuseDark.js
Normal file
19
src/@fuse/colors/fuseDark.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const fuseDark = {
|
||||
50: '#e5e6e8',
|
||||
100: '#bec1c5',
|
||||
200: '#92979f',
|
||||
300: '#666d78',
|
||||
400: '#464e5b',
|
||||
500: '#252f3e',
|
||||
600: '#212a38',
|
||||
700: '#1b2330',
|
||||
800: '#161d28',
|
||||
900: '#0d121b',
|
||||
A100: '#5d8eff',
|
||||
A200: '#2a6aff',
|
||||
A400: '#004af6',
|
||||
A700: '#0042dd',
|
||||
contrastDefaultColor: 'light',
|
||||
};
|
||||
|
||||
export default fuseDark;
|
||||
2
src/@fuse/colors/index.js
Normal file
2
src/@fuse/colors/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as fuseDark } from './fuseDark';
|
||||
export { default as skyBlue } from './skyBlue';
|
||||
19
src/@fuse/colors/skyBlue.js
Normal file
19
src/@fuse/colors/skyBlue.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const skyBlue = {
|
||||
50: '#e4fafd',
|
||||
100: '#bdf2fa',
|
||||
200: '#91e9f7',
|
||||
300: '#64e0f3',
|
||||
400: '#43daf1',
|
||||
500: '#22d3ee',
|
||||
600: '#1eceec',
|
||||
700: '#19c8e9',
|
||||
800: '#14c2e7',
|
||||
900: '#0cb7e2',
|
||||
A100: '#ffffff',
|
||||
A200: '#daf7ff',
|
||||
A400: '#a7ecff',
|
||||
A700: '#8de6ff',
|
||||
contrastDefaultColor: 'dark',
|
||||
};
|
||||
|
||||
export default skyBlue;
|
||||
25
src/@fuse/core/BrowserRouter/BrowserRouter.js
Normal file
25
src/@fuse/core/BrowserRouter/BrowserRouter.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import history from '@history';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
function BrowserRouter({ basename, children, window }) {
|
||||
const [state, setState] = useState({
|
||||
action: history.action,
|
||||
location: history.location,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => history.listen(setState), [history]);
|
||||
|
||||
return (
|
||||
<Router
|
||||
basename={basename}
|
||||
location={state.location}
|
||||
navigationType={state.action}
|
||||
navigator={history}
|
||||
>
|
||||
{children}
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrowserRouter;
|
||||
1
src/@fuse/core/BrowserRouter/index.js
Normal file
1
src/@fuse/core/BrowserRouter/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './BrowserRouter';
|
||||
121
src/@fuse/core/DemoContent/DemoContent.js
Normal file
121
src/@fuse/core/DemoContent/DemoContent.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
function DemoContent() {
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src="assets/images/demo-content/morain-lake.jpg"
|
||||
alt="beach"
|
||||
style={{
|
||||
maxWidth: '640px',
|
||||
width: '100%',
|
||||
}}
|
||||
className="rounded-6"
|
||||
/>
|
||||
<h1 className="py-16 font-semibold">Early Sunrise</h1>
|
||||
<h4 className="pb-12 font-medium">Demo Content</h4>
|
||||
<p>
|
||||
One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in
|
||||
his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a
|
||||
little he could see his brown belly, slightly domed and divided by arches into stiff
|
||||
sections.
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
The bedding was hardly able to cover it and seemed ready to slide off any moment. His many
|
||||
legs, pitifully thin compared with the size of the rest of him, waved about helplessly as
|
||||
he looked. "What's happened to me? " he thought. It wasn't a dream.
|
||||
</p>
|
||||
<footer>John Doe</footer>
|
||||
</blockquote>
|
||||
<p>
|
||||
His room, a proper human room although a little too small, lay peacefully between its four
|
||||
familiar walls. A collection of textile samples lay spread out on the table - Samsa was a
|
||||
travelling salesman - and above it there hung a picture that he had recently cut out of an
|
||||
illustrated magazine and housed in a nice, gilded frame.
|
||||
</p>
|
||||
<p>
|
||||
It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur
|
||||
muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look
|
||||
out the window at the dull weather. Drops of rain could be heard hitting the pane, which
|
||||
made him feel quite sad.
|
||||
</p>
|
||||
<p>
|
||||
"How about if I sleep a little bit longer and forget all this nonsense", he thought, but
|
||||
that was something he was unable to do because he was used to sleeping on his right, and in
|
||||
his present state couldn't get into that position. However hard he threw himself onto his
|
||||
right, he always rolled back to where he was.
|
||||
</p>
|
||||
<p>
|
||||
He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the
|
||||
floundering legs, and only stopped when he began to feel a mild, dull pain there that he had
|
||||
never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen!
|
||||
</p>
|
||||
<p>
|
||||
Travelling day in and day out. Doing business like this takes much more effort than doing
|
||||
your own business at home, and on top of that there's the curse of travelling, worries about
|
||||
making train connections, bad and irregular food, contact with different people all the time
|
||||
so that you can never get to know anyone or become friendly with them.
|
||||
</p>
|
||||
<p>
|
||||
"He felt a slight itch up on his belly; pushed himself slowly up on his back towards the
|
||||
headboard so that he could lift his head better; found where the itch was, and saw that it
|
||||
was covered with lots of little white spots which he didn't know what to make of; and when
|
||||
he tried to feel the place with one of his legs he drew it quickly back because as soon as
|
||||
he touched it he was overcome by a cold shudder. He slid back into his former position.
|
||||
</p>
|
||||
<p>
|
||||
"Getting up early all the time", he thought, "it makes you stupid. You've got to get enough
|
||||
sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to
|
||||
the guest house during the morning to copy out the contract, these gentlemen are always
|
||||
still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get
|
||||
kicked out on the spot. But who knows, maybe that would be the best thing for me...
|
||||
</p>
|
||||
<p>
|
||||
His room, a proper human room although a little too small, lay peacefully between its four
|
||||
familiar walls. A collection of textile samples lay spread out on the table - Samsa was a
|
||||
travelling salesman - and above it there hung a picture that he had recently cut out of an
|
||||
illustrated magazine and housed in a nice, gilded frame.
|
||||
</p>
|
||||
<p>
|
||||
It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur
|
||||
muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look
|
||||
out the window at the dull weather. Drops of rain could be heard hitting the pane, which
|
||||
made him feel quite sad.
|
||||
</p>
|
||||
<p>
|
||||
"How about if I sleep a little bit longer and forget all this nonsense", he thought, but
|
||||
that was something he was unable to do because he was used to sleeping on his right, and in
|
||||
his present state couldn't get into that position. However hard he threw himself onto his
|
||||
right, he always rolled back to where he was.
|
||||
</p>
|
||||
<p>
|
||||
He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the
|
||||
floundering legs, and only stopped when he began to feel a mild, dull pain there that he had
|
||||
never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen!
|
||||
</p>
|
||||
<p>
|
||||
Travelling day in and day out. Doing business like this takes much more effort than doing
|
||||
your own business at home, and on top of that there's the curse of travelling, worries about
|
||||
making train connections, bad and irregular food, contact with different people all the time
|
||||
so that you can never get to know anyone or become friendly with them.
|
||||
</p>
|
||||
<p>
|
||||
"He felt a slight itch up on his belly; pushed himself slowly up on his back towards the
|
||||
headboard so that he could lift his head better; found where the itch was, and saw that it
|
||||
was covered with lots of little white spots which he didn't know what to make of; and when
|
||||
he tried to feel the place with one of his legs he drew it quickly back because as soon as
|
||||
he touched it he was overcome by a cold shudder. He slid back into his former position.
|
||||
</p>
|
||||
<p>
|
||||
"Getting up early all the time", he thought, "it makes you stupid. You've got to get enough
|
||||
sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to
|
||||
the guest house during the morning to copy out the contract, these gentlemen are always
|
||||
still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get
|
||||
kicked out on the spot. But who knows, maybe that would be the best thing for me...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DemoContent);
|
||||
1
src/@fuse/core/DemoContent/index.js
Normal file
1
src/@fuse/core/DemoContent/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DemoContent';
|
||||
29
src/@fuse/core/DemoSidebarContent/DemoSidebarContent.js
Normal file
29
src/@fuse/core/DemoSidebarContent/DemoSidebarContent.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import _ from '@lodash';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { cloneElement, memo } from 'react';
|
||||
|
||||
function DemoSidebarContent() {
|
||||
function generate(element) {
|
||||
return _(30).times((value) =>
|
||||
cloneElement(element, {
|
||||
key: value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<List dense>
|
||||
{generate(
|
||||
<ListItem button>
|
||||
<ListItemText primary="Single-line item" />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DemoSidebarContent);
|
||||
1
src/@fuse/core/DemoSidebarContent/index.js
Normal file
1
src/@fuse/core/DemoSidebarContent/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DemoSidebarContent';
|
||||
80
src/@fuse/core/FuseAuthorization/FuseAuthorization.js
Normal file
80
src/@fuse/core/FuseAuthorization/FuseAuthorization.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import FuseUtils from '@fuse/utils';
|
||||
import AppContext from 'app/AppContext';
|
||||
import { Component } from 'react';
|
||||
import { matchRoutes } from 'react-router-dom';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import history from '@history';
|
||||
|
||||
let loginRedirectUrl = null;
|
||||
|
||||
class FuseAuthorization extends Component {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
const { routes } = context;
|
||||
this.state = {
|
||||
accessGranted: true,
|
||||
routes,
|
||||
};
|
||||
this.defaultLoginRedirectUrl = props.loginRedirectUrl || '/';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.state.accessGranted) {
|
||||
this.redirectRoute();
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.accessGranted !== this.state.accessGranted;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.accessGranted) {
|
||||
this.redirectRoute();
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { location, userRole } = props;
|
||||
const { pathname } = location;
|
||||
|
||||
const matchedRoutes = matchRoutes(state.routes, pathname);
|
||||
|
||||
const matched = matchedRoutes ? matchedRoutes[0] : false;
|
||||
return {
|
||||
accessGranted: matched ? FuseUtils.hasPermission(matched.route.auth, userRole) : true,
|
||||
};
|
||||
}
|
||||
|
||||
redirectRoute() {
|
||||
const { location, userRole } = this.props;
|
||||
const { pathname } = location;
|
||||
const redirectUrl = loginRedirectUrl || this.defaultLoginRedirectUrl;
|
||||
|
||||
/*
|
||||
User is guest
|
||||
Redirect to Login Page
|
||||
*/
|
||||
if (!userRole || userRole.length === 0) {
|
||||
setTimeout(() => history.push('/sign-in'), 0);
|
||||
loginRedirectUrl = pathname;
|
||||
} else {
|
||||
/*
|
||||
User is member
|
||||
User must be on unAuthorized page or just logged in
|
||||
Redirect to dashboard or loginRedirectUrl
|
||||
*/
|
||||
setTimeout(() => history.push(redirectUrl), 0);
|
||||
loginRedirectUrl = this.defaultLoginRedirectUrl;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// console.info('Fuse Authorization rendered', this.state.accessGranted);
|
||||
return this.state.accessGranted ? <>{this.props.children}</> : null;
|
||||
}
|
||||
}
|
||||
|
||||
FuseAuthorization.contextType = AppContext;
|
||||
|
||||
export default withRouter(FuseAuthorization);
|
||||
1
src/@fuse/core/FuseAuthorization/index.js
Normal file
1
src/@fuse/core/FuseAuthorization/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseAuthorization';
|
||||
97
src/@fuse/core/FuseCountdown/FuseCountdown.js
Normal file
97
src/@fuse/core/FuseCountdown/FuseCountdown.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import clsx from 'clsx';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
function FuseCountdown(props) {
|
||||
const { onComplete } = props;
|
||||
const [endDate] = useState(
|
||||
moment.isMoment(props.endDate) ? props.endDate : moment(props.endDate)
|
||||
);
|
||||
const [countdown, setCountdown] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
const intervalRef = useRef();
|
||||
|
||||
const complete = useCallback(() => {
|
||||
window.clearInterval(intervalRef.current);
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}, [onComplete]);
|
||||
|
||||
const tick = useCallback(() => {
|
||||
const currDate = moment();
|
||||
const diff = endDate.diff(currDate, 'seconds');
|
||||
if (diff < 0) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
const timeLeft = moment.duration(diff, 'seconds');
|
||||
setCountdown({
|
||||
days: timeLeft.asDays().toFixed(0),
|
||||
hours: timeLeft.hours(),
|
||||
minutes: timeLeft.minutes(),
|
||||
seconds: timeLeft.seconds(),
|
||||
});
|
||||
}, [complete, endDate]);
|
||||
|
||||
useEffect(() => {
|
||||
intervalRef.current = setInterval(tick, 1000);
|
||||
return () => {
|
||||
clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [tick]);
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-center', props.className)}>
|
||||
<div className="flex flex-col items-center justify-center px-12">
|
||||
<Typography variant="h4" className="mb-4">
|
||||
{countdown.days}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
days
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center px-12">
|
||||
<Typography variant="h4" className="mb-4">
|
||||
{countdown.hours}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
hours
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center px-12">
|
||||
<Typography variant="h4" className="mb-4">
|
||||
{countdown.minutes}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
minutes
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center px-12">
|
||||
<Typography variant="h4" className="mb-4">
|
||||
{countdown.seconds}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
seconds
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FuseCountdown.propTypes = {
|
||||
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
onComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
FuseCountdown.defaultProps = {
|
||||
endDate: moment().add(15, 'days'),
|
||||
};
|
||||
|
||||
export default memo(FuseCountdown);
|
||||
1
src/@fuse/core/FuseCountdown/index.js
Normal file
1
src/@fuse/core/FuseCountdown/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseCountdown';
|
||||
27
src/@fuse/core/FuseDialog/FuseDialog.js
Normal file
27
src/@fuse/core/FuseDialog/FuseDialog.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
closeDialog,
|
||||
selectFuseDialogOptions,
|
||||
selectFuseDialogState,
|
||||
} from 'app/store/fuse/dialogSlice';
|
||||
|
||||
function FuseDialog(props) {
|
||||
const dispatch = useDispatch();
|
||||
const state = useSelector(selectFuseDialogState);
|
||||
const options = useSelector(selectFuseDialogOptions);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={state}
|
||||
onClose={(ev) => dispatch(closeDialog())}
|
||||
aria-labelledby="fuse-dialog-title"
|
||||
classes={{
|
||||
paper: 'rounded-8',
|
||||
}}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FuseDialog;
|
||||
1
src/@fuse/core/FuseDialog/index.js
Normal file
1
src/@fuse/core/FuseDialog/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseDialog';
|
||||
110
src/@fuse/core/FuseExample/DemoFrame.js
Normal file
110
src/@fuse/core/FuseExample/DemoFrame.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import rtlPlugin from 'stylis-plugin-rtl';
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { StyleSheetManager } from 'styled-components';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
|
||||
function FramedDemo(props) {
|
||||
const { children, document } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
React.useEffect(() => {
|
||||
document.body.dir = theme.direction;
|
||||
}, [document, theme.direction]);
|
||||
|
||||
const cache = React.useMemo(
|
||||
() =>
|
||||
createCache({
|
||||
key: `iframe-demo-${theme.direction}`,
|
||||
prepend: true,
|
||||
container: document.head,
|
||||
stylisPlugins: theme.direction === 'rtl' ? [rtlPlugin] : [],
|
||||
}),
|
||||
[document, theme.direction]
|
||||
);
|
||||
|
||||
const getWindow = React.useCallback(() => document.defaultView, [document]);
|
||||
|
||||
return (
|
||||
<StyleSheetManager
|
||||
target={document.head}
|
||||
stylisPlugins={theme.direction === 'rtl' ? [rtlPlugin] : []}
|
||||
>
|
||||
<CacheProvider value={cache}>
|
||||
<GlobalStyles
|
||||
styles={() => ({
|
||||
html: {
|
||||
fontSize: '62.5%',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{React.cloneElement(children, {
|
||||
window: getWindow,
|
||||
})}
|
||||
</CacheProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
|
||||
FramedDemo.propTypes = {
|
||||
children: PropTypes.node,
|
||||
document: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const Frame = styled('iframe')(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
flexGrow: 1,
|
||||
height: 400,
|
||||
border: 0,
|
||||
boxShadow: theme.shadows[1],
|
||||
}));
|
||||
|
||||
function DemoFrame(props) {
|
||||
const { children, name, ...other } = props;
|
||||
const title = `${name} demo`;
|
||||
/**
|
||||
* @type {import('react').Ref<HTMLIFrameElement>}
|
||||
*/
|
||||
const frameRef = React.useRef(null);
|
||||
|
||||
// If we portal content into the iframe before the load event then that content
|
||||
// is dropped in firefox.
|
||||
const [iframeLoaded, onLoad] = React.useReducer(() => true, false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const document = frameRef.current.contentDocument;
|
||||
// When we hydrate the iframe then the load event is already dispatched
|
||||
// once the iframe markup is parsed (maybe later but the important part is
|
||||
// that it happens before React can attach event listeners).
|
||||
// We need to check the readyState of the document once the iframe is mounted
|
||||
// and "replay" the missed load event.
|
||||
// See https://github.com/facebook/react/pull/13862 for ongoing effort in React
|
||||
// (though not with iframes in mind).
|
||||
if (document != null && document.readyState === 'complete' && !iframeLoaded) {
|
||||
onLoad();
|
||||
}
|
||||
}, [iframeLoaded]);
|
||||
const document = frameRef.current?.contentDocument;
|
||||
return (
|
||||
<>
|
||||
<Frame onLoad={onLoad} ref={frameRef} title={title} {...other} />
|
||||
{iframeLoaded !== false
|
||||
? ReactDOM.createPortal(
|
||||
<FramedDemo document={document}>{children}</FramedDemo>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
DemoFrame.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(DemoFrame);
|
||||
106
src/@fuse/core/FuseExample/FuseExample.js
Normal file
106
src/@fuse/core/FuseExample/FuseExample.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import FuseHighlight from '@fuse/core/FuseHighlight';
|
||||
import Card from '@mui/material/Card';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { darken } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import DemoFrame from './DemoFrame';
|
||||
import FuseSvgIcon from '../FuseSvgIcon';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
raw: PropTypes.object,
|
||||
currentTabIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
name: '',
|
||||
currentTabIndex: 0,
|
||||
};
|
||||
|
||||
function FuseExample(props) {
|
||||
const [currentTab, setCurrentTab] = useState(props.currentTabIndex);
|
||||
const { component: Component, raw, iframe, className, name } = props;
|
||||
|
||||
function handleChange(event, value) {
|
||||
setCurrentTab(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={clsx(className, 'shadow')}
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
darken(theme.palette.background.paper, theme.palette.mode === 'light' ? 0.01 : 0.1),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
darken(theme.palette.background.paper, theme.palette.mode === 'light' ? 0.02 : 0.2),
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
classes={{
|
||||
root: 'border-b-1',
|
||||
flexContainer: 'justify-end',
|
||||
}}
|
||||
value={currentTab}
|
||||
onChange={handleChange}
|
||||
textColor="secondary"
|
||||
indicatorColor="secondary"
|
||||
>
|
||||
{Component && (
|
||||
<Tab
|
||||
classes={{ root: 'min-w-64' }}
|
||||
icon={<FuseSvgIcon>heroicons-outline:eye</FuseSvgIcon>}
|
||||
/>
|
||||
)}
|
||||
{raw && (
|
||||
<Tab
|
||||
classes={{ root: 'min-w-64' }}
|
||||
icon={<FuseSvgIcon>heroicons-outline:code</FuseSvgIcon>}
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
<div className="flex justify-center max-w-full relative">
|
||||
<div className={currentTab === 0 ? 'flex flex-1 max-w-full' : 'hidden'}>
|
||||
{Component &&
|
||||
(iframe ? (
|
||||
<DemoFrame name={name}>
|
||||
<Component />
|
||||
</DemoFrame>
|
||||
) : (
|
||||
<div className="p-24 flex flex-1 justify-center max-w-full">
|
||||
<Component />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={currentTab === 1 ? 'flex flex-1' : 'hidden'}>
|
||||
{useMemo(() => {
|
||||
return raw && currentTab === 1 ? (
|
||||
<div className="flex flex-1">
|
||||
<FuseHighlight
|
||||
component="pre"
|
||||
className="language-javascript w-full"
|
||||
sx={{ borderRadius: '0!important' }}
|
||||
>
|
||||
{raw.default}
|
||||
</FuseHighlight>
|
||||
</div>
|
||||
) : null;
|
||||
}, [raw, currentTab])}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
FuseExample.propTypes = propTypes;
|
||||
FuseExample.defaultProps = defaultProps;
|
||||
|
||||
export default FuseExample;
|
||||
1
src/@fuse/core/FuseExample/index.js
Normal file
1
src/@fuse/core/FuseExample/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseExample';
|
||||
82
src/@fuse/core/FuseHighlight/FuseHighlight.js
Normal file
82
src/@fuse/core/FuseHighlight/FuseHighlight.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as Prism from 'prismjs';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import './prism-languages';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FuseHighlight(props) {
|
||||
const { async, children, className, component: Wrapper } = props;
|
||||
|
||||
const domNode = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (domNode.current) {
|
||||
Prism.highlightElement(domNode.current, async);
|
||||
}
|
||||
}, [children, async]);
|
||||
|
||||
return useMemo(() => {
|
||||
const trimCode = () => {
|
||||
let sourceString = children;
|
||||
|
||||
if (typeof sourceString === 'object' && sourceString.default) {
|
||||
sourceString = sourceString.default;
|
||||
}
|
||||
|
||||
// Split the source into lines
|
||||
const sourceLines = sourceString.split('\n');
|
||||
|
||||
// Remove the first and the last line of the source
|
||||
// code if they are blank lines. This way, the html
|
||||
// can be formatted properly while using fuse-highlight
|
||||
// component
|
||||
if (!sourceLines[0].trim()) {
|
||||
sourceLines.shift();
|
||||
}
|
||||
|
||||
if (!sourceLines[sourceLines.length - 1].trim()) {
|
||||
sourceLines.pop();
|
||||
}
|
||||
|
||||
// Find the first non-whitespace char index in
|
||||
// the first line of the source code
|
||||
const indexOfFirstChar = sourceLines[0].search(/\S|$/);
|
||||
|
||||
// Generate the trimmed source
|
||||
let sourceRaw = '';
|
||||
|
||||
// Iterate through all the lines
|
||||
sourceLines.forEach((line, index) => {
|
||||
// Trim the beginning white space depending on the index
|
||||
// and concat the source code
|
||||
sourceRaw += line.substr(indexOfFirstChar, line.length);
|
||||
|
||||
// If it's not the last line...
|
||||
if (index !== sourceLines.length - 1) {
|
||||
// Add a line break at the end
|
||||
sourceRaw = `${sourceRaw}\n`;
|
||||
}
|
||||
});
|
||||
return sourceRaw || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper ref={domNode} className={clsx('border', className)}>
|
||||
{/* {trimCode()} */}
|
||||
{trimCode()}
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
}, [children, className]);
|
||||
}
|
||||
|
||||
FuseHighlight.propTypes = {
|
||||
component: PropTypes.node,
|
||||
};
|
||||
FuseHighlight.defaultProps = {
|
||||
component: 'code',
|
||||
};
|
||||
|
||||
export default styled(FuseHighlight)``;
|
||||
1
src/@fuse/core/FuseHighlight/index.js
Normal file
1
src/@fuse/core/FuseHighlight/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseHighlight';
|
||||
18
src/@fuse/core/FuseHighlight/prism-languages.js
Normal file
18
src/@fuse/core/FuseHighlight/prism-languages.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'prismjs/components/prism-c';
|
||||
import 'prismjs/components/prism-cpp';
|
||||
import 'prismjs/components/prism-csharp';
|
||||
import 'prismjs/components/prism-css';
|
||||
import 'prismjs/components/prism-diff';
|
||||
import 'prismjs/components/prism-java';
|
||||
import 'prismjs/components/prism-javascript';
|
||||
import 'prismjs/components/prism-json';
|
||||
import 'prismjs/components/prism-jsx';
|
||||
import 'prismjs/components/prism-markup';
|
||||
import 'prismjs/components/prism-markup-templating';
|
||||
import 'prismjs/components/prism-perl';
|
||||
import 'prismjs/components/prism-php';
|
||||
import 'prismjs/components/prism-python';
|
||||
import 'prismjs/components/prism-sass';
|
||||
import 'prismjs/components/prism-scss';
|
||||
import 'prismjs/components/prism-typescript';
|
||||
import 'prismjs/prism';
|
||||
148
src/@fuse/core/FuseLayout/FuseLayout.js
Normal file
148
src/@fuse/core/FuseLayout/FuseLayout.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useDeepCompareEffect } from '@fuse/hooks';
|
||||
import _ from '@lodash';
|
||||
import AppContext from 'app/AppContext';
|
||||
import {
|
||||
generateSettings,
|
||||
selectFuseCurrentSettings,
|
||||
selectFuseDefaultSettings,
|
||||
setSettings,
|
||||
} from 'app/store/fuse/settingsSlice';
|
||||
import { memo, useCallback, useContext, useMemo, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { matchRoutes, useLocation } from 'react-router-dom';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
|
||||
const inputGlobalStyles = (
|
||||
<GlobalStyles
|
||||
styles={(theme) => ({
|
||||
html: {
|
||||
backgroundColor: `${theme.palette.background.default}!important`,
|
||||
color: `${theme.palette.text.primary}!important`,
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
/* 'code:not([class*="language-"])': {
|
||||
color: theme.palette.secondary.dark,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light' ? 'rgba(255, 255, 255, .9)' : 'rgba(0, 0, 0, .9)',
|
||||
padding: '2px 3px',
|
||||
borderRadius: 2,
|
||||
lineHeight: 1.7,
|
||||
}, */
|
||||
'table.simple tbody tr th': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
'table.simple thead tr th': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
'a:not([role=button]):not(.MuiButtonBase-root)': {
|
||||
color: theme.palette.secondary.main,
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {},
|
||||
},
|
||||
'a.link, a:not([role=button])[target=_blank]': {
|
||||
background: alpha(theme.palette.secondary.main, 0.2),
|
||||
color: 'inherit',
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
background: alpha(theme.palette.secondary.main, 0.3),
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
'[class^="border"]': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
'[class*="border"]': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
'[class*="divide-"] > :not([hidden]) ~ :not([hidden])': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
hr: {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
|
||||
'::-webkit-scrollbar-thumb': {
|
||||
boxShadow: `inset 0 0 0 20px ${
|
||||
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
|
||||
}`,
|
||||
},
|
||||
'::-webkit-scrollbar-thumb:active': {
|
||||
boxShadow: `inset 0 0 0 20px ${
|
||||
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
|
||||
}`,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
function FuseLayout(props) {
|
||||
const { layouts } = props;
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector(selectFuseCurrentSettings);
|
||||
const defaultSettings = useSelector(selectFuseDefaultSettings);
|
||||
|
||||
const appContext = useContext(AppContext);
|
||||
const { routes } = appContext;
|
||||
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
const matchedRoutes = matchRoutes(routes, pathname);
|
||||
const matched = matchedRoutes ? matchedRoutes[0] : false;
|
||||
|
||||
const newSettings = useRef(null);
|
||||
|
||||
const shouldAwaitRender = useCallback(() => {
|
||||
let _newSettings;
|
||||
/**
|
||||
* On Path changed
|
||||
*/
|
||||
// if (prevPathname !== pathname) {
|
||||
if (matched && matched.route.settings) {
|
||||
/**
|
||||
* if matched route has settings
|
||||
*/
|
||||
|
||||
const routeSettings = matched.route.settings;
|
||||
|
||||
_newSettings = generateSettings(defaultSettings, routeSettings);
|
||||
} else if (!_.isEqual(newSettings.current, defaultSettings)) {
|
||||
/**
|
||||
* Reset to default settings on the new path
|
||||
*/
|
||||
_newSettings = _.merge({}, defaultSettings);
|
||||
} else {
|
||||
_newSettings = newSettings.current;
|
||||
}
|
||||
|
||||
if (!_.isEqual(newSettings.current, _newSettings)) {
|
||||
newSettings.current = _newSettings;
|
||||
}
|
||||
}, [defaultSettings, matched]);
|
||||
|
||||
shouldAwaitRender();
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (!_.isEqual(newSettings.current, settings)) {
|
||||
dispatch(setSettings(newSettings.current));
|
||||
}
|
||||
}, [dispatch, newSettings.current, settings]);
|
||||
|
||||
// console.warn('::FuseLayout:: rendered');
|
||||
|
||||
const Layout = useMemo(() => layouts[settings.layout.style], [layouts, settings.layout.style]);
|
||||
|
||||
return _.isEqual(newSettings.current, settings) ? (
|
||||
<>
|
||||
{inputGlobalStyles}
|
||||
<Layout {...props} />
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default memo(FuseLayout);
|
||||
1
src/@fuse/core/FuseLayout/index.js
Normal file
1
src/@fuse/core/FuseLayout/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseLayout';
|
||||
49
src/@fuse/core/FuseLoading/FuseLoading.js
Normal file
49
src/@fuse/core/FuseLoading/FuseLoading.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useTimeout } from '@fuse/hooks';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
function FuseLoading(props) {
|
||||
const [showLoading, setShowLoading] = useState(!props.delay);
|
||||
|
||||
useTimeout(() => {
|
||||
setShowLoading(true);
|
||||
}, props.delay);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-1 flex-col items-center justify-center p-24',
|
||||
!showLoading && 'hidden'
|
||||
)}
|
||||
>
|
||||
<Typography className="text-13 sm:text-20 font-medium -mb-16" color="text.secondary">
|
||||
Loading
|
||||
</Typography>
|
||||
<Box
|
||||
id="spinner"
|
||||
sx={{
|
||||
'& > div': {
|
||||
backgroundColor: 'palette.secondary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="bounce1" />
|
||||
<div className="bounce2" />
|
||||
<div className="bounce3" />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FuseLoading.propTypes = {
|
||||
delay: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
|
||||
};
|
||||
|
||||
FuseLoading.defaultProps = {
|
||||
delay: false,
|
||||
};
|
||||
|
||||
export default FuseLoading;
|
||||
1
src/@fuse/core/FuseLoading/index.js
Normal file
1
src/@fuse/core/FuseLoading/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseLoading';
|
||||
91
src/@fuse/core/FuseMessage/FuseMessage.js
Normal file
91
src/@fuse/core/FuseMessage/FuseMessage.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { amber, blue, green } from '@mui/material/colors';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import SnackbarContent from '@mui/material/SnackbarContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { memo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
hideMessage,
|
||||
selectFuseMessageOptions,
|
||||
selectFuseMessageState,
|
||||
} from 'app/store/fuse/messageSlice';
|
||||
import FuseSvgIcon from '../FuseSvgIcon';
|
||||
|
||||
const StyledSnackbar = styled(Snackbar)(({ theme, variant }) => ({
|
||||
'& .FuseMessage-content': {
|
||||
...(variant === 'success' && {
|
||||
backgroundColor: green[600],
|
||||
color: '#FFFFFF',
|
||||
}),
|
||||
|
||||
...(variant === 'error' && {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: theme.palette.getContrastText(theme.palette.error.dark),
|
||||
}),
|
||||
|
||||
...(variant === 'info' && {
|
||||
backgroundColor: blue[600],
|
||||
color: '#FFFFFF',
|
||||
}),
|
||||
|
||||
...(variant === 'warning' && {
|
||||
backgroundColor: amber[600],
|
||||
color: '#FFFFFF',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const variantIcon = {
|
||||
success: 'check_circle',
|
||||
warning: 'warning',
|
||||
error: 'error_outline',
|
||||
info: 'info',
|
||||
};
|
||||
|
||||
function FuseMessage(props) {
|
||||
const dispatch = useDispatch();
|
||||
const state = useSelector(selectFuseMessageState);
|
||||
const options = useSelector(selectFuseMessageOptions);
|
||||
|
||||
return (
|
||||
<StyledSnackbar
|
||||
{...options}
|
||||
open={state}
|
||||
onClose={() => dispatch(hideMessage())}
|
||||
ContentProps={{
|
||||
variant: 'body2',
|
||||
headlineMapping: {
|
||||
body1: 'div',
|
||||
body2: 'div',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SnackbarContent
|
||||
className="FuseMessage-content"
|
||||
message={
|
||||
<div className="flex items-center">
|
||||
{variantIcon[options.variant] && (
|
||||
<FuseSvgIcon color="inherit">{variantIcon[options.variant]}</FuseSvgIcon>
|
||||
)}
|
||||
<Typography className="mx-8">{options.message}</Typography>
|
||||
</div>
|
||||
}
|
||||
action={[
|
||||
<IconButton
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
onClick={() => dispatch(hideMessage())}
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
|
||||
</IconButton>,
|
||||
]}
|
||||
/>
|
||||
</StyledSnackbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FuseMessage);
|
||||
1
src/@fuse/core/FuseMessage/index.js
Normal file
1
src/@fuse/core/FuseMessage/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseMessage';
|
||||
44
src/@fuse/core/FuseNavigation/FuseNavBadge.js
Normal file
44
src/@fuse/core/FuseNavigation/FuseNavBadge.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
padding: '0 7px',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
height: 20,
|
||||
minWidth: 20,
|
||||
borderRadius: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.secondary.contrastText,
|
||||
}));
|
||||
|
||||
function FuseNavBadge(props) {
|
||||
const { className, badge } = props;
|
||||
|
||||
return (
|
||||
<Root
|
||||
className={clsx('item-badge', className, badge?.classes)}
|
||||
style={{
|
||||
backgroundColor: badge.bg,
|
||||
color: badge.fg,
|
||||
}}
|
||||
>
|
||||
{badge.title}
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavBadge.propTypes = {
|
||||
badge: PropTypes.shape({
|
||||
title: PropTypes.node,
|
||||
bg: PropTypes.string,
|
||||
fg: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
FuseNavBadge.defaultProps = {};
|
||||
|
||||
export default memo(FuseNavBadge);
|
||||
10
src/@fuse/core/FuseNavigation/FuseNavItem.js
Normal file
10
src/@fuse/core/FuseNavigation/FuseNavItem.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const components = {};
|
||||
|
||||
export function registerComponent(name, Component) {
|
||||
components[name] = Component;
|
||||
}
|
||||
|
||||
export default function FuseNavItem(props) {
|
||||
const C = components[props.type];
|
||||
return C ? <C {...props} /> : null;
|
||||
}
|
||||
91
src/@fuse/core/FuseNavigation/FuseNavigation.js
Normal file
91
src/@fuse/core/FuseNavigation/FuseNavigation.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import Divider from '@mui/material/Divider';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo } from 'react';
|
||||
import _ from '@lodash';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import FuseNavHorizontalLayout1 from './horizontal/FuseNavHorizontalLayout1';
|
||||
import FuseNavVerticalLayout1 from './vertical/FuseNavVerticalLayout1';
|
||||
import FuseNavVerticalLayout2 from './vertical/FuseNavVerticalLayout2';
|
||||
import FuseNavHorizontalCollapse from './horizontal/types/FuseNavHorizontalCollapse';
|
||||
import FuseNavHorizontalGroup from './horizontal/types/FuseNavHorizontalGroup';
|
||||
import FuseNavHorizontalItem from './horizontal/types/FuseNavHorizontalItem';
|
||||
import FuseNavHorizontalLink from './horizontal/types/FuseNavHorizontalLink';
|
||||
import FuseNavVerticalCollapse from './vertical/types/FuseNavVerticalCollapse';
|
||||
import FuseNavVerticalGroup from './vertical/types/FuseNavVerticalGroup';
|
||||
import FuseNavVerticalItem from './vertical/types/FuseNavVerticalItem';
|
||||
import FuseNavVerticalLink from './vertical/types/FuseNavVerticalLink';
|
||||
import { registerComponent } from './FuseNavItem';
|
||||
|
||||
const inputGlobalStyles = (
|
||||
<GlobalStyles
|
||||
styles={(theme) => ({
|
||||
'.popper-navigation-list': {
|
||||
'& .fuse-list-item': {
|
||||
padding: '8px 12px 8px 12px',
|
||||
height: 40,
|
||||
minHeight: 40,
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 8px',
|
||||
},
|
||||
},
|
||||
'&.dense': {
|
||||
'& .fuse-list-item': {
|
||||
minHeight: 32,
|
||||
height: 32,
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
/*
|
||||
Register Fuse Navigation Components
|
||||
*/
|
||||
registerComponent('vertical-group', FuseNavVerticalGroup);
|
||||
registerComponent('vertical-collapse', FuseNavVerticalCollapse);
|
||||
registerComponent('vertical-item', FuseNavVerticalItem);
|
||||
registerComponent('vertical-link', FuseNavVerticalLink);
|
||||
registerComponent('horizontal-group', FuseNavHorizontalGroup);
|
||||
registerComponent('horizontal-collapse', FuseNavHorizontalCollapse);
|
||||
registerComponent('horizontal-item', FuseNavHorizontalItem);
|
||||
registerComponent('horizontal-link', FuseNavHorizontalLink);
|
||||
registerComponent('vertical-divider', () => <Divider className="my-16" />);
|
||||
registerComponent('horizontal-divider', () => <Divider className="my-16" />);
|
||||
|
||||
function FuseNavigation(props) {
|
||||
const options = _.pick(props, [
|
||||
'navigation',
|
||||
'layout',
|
||||
'active',
|
||||
'dense',
|
||||
'className',
|
||||
'onItemClick',
|
||||
'firstLevel',
|
||||
'selectedId',
|
||||
]);
|
||||
if (props.navigation.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{inputGlobalStyles}
|
||||
{props.layout === 'horizontal' && <FuseNavHorizontalLayout1 {...options} />}
|
||||
{props.layout === 'vertical' && <FuseNavVerticalLayout1 {...options} />}
|
||||
{props.layout === 'vertical-2' && <FuseNavVerticalLayout2 {...options} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
FuseNavigation.propTypes = {
|
||||
navigation: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
FuseNavigation.defaultProps = {
|
||||
layout: 'vertical',
|
||||
};
|
||||
|
||||
export default memo(FuseNavigation);
|
||||
@@ -0,0 +1,59 @@
|
||||
import List from '@mui/material/List';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import FuseNavItem from '../FuseNavItem';
|
||||
|
||||
const StyledList = styled(List)(({ theme }) => ({
|
||||
'& .fuse-list-item': {
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
|
||||
},
|
||||
'&:focus:not(.active)': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
|
||||
},
|
||||
padding: '8px 12px 8px 12px',
|
||||
height: 40,
|
||||
minHeight: 40,
|
||||
'&.level-0': {
|
||||
minHeight: 44,
|
||||
minminHeight: 44,
|
||||
},
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 8px',
|
||||
},
|
||||
},
|
||||
'&.active-square-list': {
|
||||
'& .fuse-list-item': {
|
||||
borderRadius: '0',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseNavHorizontalLayout1(props) {
|
||||
const { navigation, layout, active, dense, className } = props;
|
||||
|
||||
return (
|
||||
<StyledList
|
||||
className={clsx(
|
||||
'navigation whitespace-nowrap flex p-0',
|
||||
`active-${active}-list`,
|
||||
dense && 'dense',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{navigation.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`horizontal-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={0}
|
||||
dense={dense}
|
||||
/>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
}
|
||||
|
||||
export default FuseNavHorizontalLayout1;
|
||||
@@ -0,0 +1,190 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { useDebounce } from '@fuse/hooks';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseNavItem from '../../FuseNavItem';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
minHeight: 48,
|
||||
'&.active, &.active:hover, &.active:focus': {
|
||||
backgroundColor: `${theme.palette.secondary.main}!important`,
|
||||
color: `${theme.palette.secondary.contrastText}!important`,
|
||||
|
||||
'&.open': {
|
||||
backgroundColor: 'rgba(0,0,0,.08)',
|
||||
},
|
||||
|
||||
'& > .fuse-list-item-text': {
|
||||
padding: '0 0 0 16px',
|
||||
},
|
||||
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function isUrlInChildren(parent, url) {
|
||||
if (!parent.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parent.children.length; i += 1) {
|
||||
if (parent.children[i].children) {
|
||||
if (isUrlInChildren(parent.children[i], url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function FuseNavHorizontalCollapse(props) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { item, nestedLevel, dense } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const handleToggle = useDebounce((open) => {
|
||||
setOpened(open);
|
||||
}, 150);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<ul className="relative px-0">
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div ref={ref}>
|
||||
<StyledListItem
|
||||
button
|
||||
className={clsx(
|
||||
'fuse-list-item',
|
||||
opened && 'open',
|
||||
isUrlInChildren(item, props.location.pathname) && 'active'
|
||||
)}
|
||||
onMouseEnter={() => handleToggle(true)}
|
||||
onMouseLeave={() => handleToggle(false)}
|
||||
aria-owns={opened ? 'menu-fuse-list-grow' : null}
|
||||
aria-haspopup="true"
|
||||
component={item.url ? NavLinkAdapter : 'li'}
|
||||
to={item.url}
|
||||
end={item.end}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
color="action"
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
classes={{ primary: 'text-13 truncate' }}
|
||||
/>
|
||||
|
||||
{item.badge && <FuseNavBadge className="mx-4" badge={item.badge} />}
|
||||
<IconButton
|
||||
disableRipple
|
||||
className="w-16 h-16 ltr:ml-4 rtl:mr-4 p-0"
|
||||
color="inherit"
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon size={16} className="arrow-icon">
|
||||
{theme.direction === 'ltr'
|
||||
? 'heroicons-outline:arrow-sm-right'
|
||||
: 'heroicons-outline:arrow-sm-left'}
|
||||
</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</StyledListItem>
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
{ReactDOM.createPortal(
|
||||
<Popper
|
||||
placement={theme.direction === 'ltr' ? 'right' : 'left'}
|
||||
eventsEnabled={opened}
|
||||
positionFixed
|
||||
>
|
||||
{({ ref, style, placement, arrowProps }) =>
|
||||
opened && (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
zIndex: 999 + nestedLevel + 1,
|
||||
}}
|
||||
data-placement={placement}
|
||||
className={clsx('z-999', !opened && 'pointer-events-none')}
|
||||
>
|
||||
<Grow in={opened} id="menu-fuse-list-grow" style={{ transformOrigin: '0 0 0' }}>
|
||||
<Paper
|
||||
className="rounded-8"
|
||||
onMouseEnter={() => handleToggle(true)}
|
||||
onMouseLeave={() => handleToggle(false)}
|
||||
>
|
||||
{item.children && (
|
||||
<ul className={clsx('popper-navigation-list', dense && 'dense', 'px-0')}>
|
||||
{item.children.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`horizontal-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={nestedLevel + 1}
|
||||
dense={dense}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Paper>
|
||||
</Grow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Popper>,
|
||||
document.querySelector('#root')
|
||||
)}
|
||||
</Manager>
|
||||
</ul>
|
||||
),
|
||||
[dense, handleToggle, item, nestedLevel, opened, props.location.pathname, theme.direction]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavHorizontalCollapse.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavHorizontalCollapse.defaultProps = {};
|
||||
|
||||
const NavHorizontalCollapse = withRouter(memo(FuseNavHorizontalCollapse));
|
||||
|
||||
export default NavHorizontalCollapse;
|
||||
@@ -0,0 +1,194 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { useDebounce } from '@fuse/hooks';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import FuseNavItem from '../../FuseNavItem';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
'&.active, &.active:hover, &.active:focus': {
|
||||
backgroundColor: `${theme.palette.secondary.main}!important`,
|
||||
color: `${theme.palette.secondary.contrastText}!important`,
|
||||
'& .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 16px',
|
||||
},
|
||||
'&.level-0': {
|
||||
minHeight: 44,
|
||||
borderRadius: 4,
|
||||
'&:hover': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function isUrlInChildren(parent, url) {
|
||||
if (!parent.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parent.children.length; i += 1) {
|
||||
if (parent.children[i].children) {
|
||||
if (isUrlInChildren(parent.children[i], url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function FuseNavHorizontalGroup(props) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { item, nestedLevel, dense } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const handleToggle = useDebounce((open) => {
|
||||
setOpened(open);
|
||||
}, 150);
|
||||
|
||||
return useMemo(() => {
|
||||
let popperPlacement = 'left';
|
||||
|
||||
if (nestedLevel === 0) {
|
||||
popperPlacement = theme.direction === 'ltr' ? 'bottom-start' : 'bottom-end';
|
||||
} else {
|
||||
popperPlacement = theme.direction === 'ltr' ? 'right' : 'left';
|
||||
}
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div ref={ref}>
|
||||
<StyledListItem
|
||||
button
|
||||
className={clsx(
|
||||
'fuse-list-item',
|
||||
'relative',
|
||||
`level-${nestedLevel}`,
|
||||
isUrlInChildren(item, props.location.pathname) && 'active'
|
||||
)}
|
||||
onMouseEnter={() => handleToggle(true)}
|
||||
onMouseLeave={() => handleToggle(false)}
|
||||
aria-owns={opened ? 'menu-fuse-list-grow' : null}
|
||||
aria-haspopup="true"
|
||||
component={item.url ? NavLinkAdapter : 'li'}
|
||||
to={item.url}
|
||||
end={item.end}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
color="action"
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
classes={{ primary: 'text-13 truncate' }}
|
||||
/>
|
||||
|
||||
{nestedLevel > 0 && (
|
||||
<IconButton
|
||||
disableRipple
|
||||
className="w-16 h-16 ltr:ml-4 rtl:mr-4 p-0"
|
||||
color="inherit"
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon size={16} className="arrow-icon">
|
||||
{theme.direction === 'ltr'
|
||||
? 'heroicons-outline:arrow-sm-right'
|
||||
: 'heroicons-outline:arrow-sm-left'}
|
||||
</FuseSvgIcon>
|
||||
</IconButton>
|
||||
)}
|
||||
</StyledListItem>
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
{ReactDOM.createPortal(
|
||||
<Popper placement={popperPlacement} eventsEnabled={opened} positionFixed>
|
||||
{({ ref, style, placement, arrowProps }) =>
|
||||
opened && (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
zIndex: 999 + nestedLevel,
|
||||
}}
|
||||
data-placement={placement}
|
||||
className={clsx('z-999', !opened && 'pointer-events-none')}
|
||||
>
|
||||
<Grow in={opened} id="menu-fuse-list-grow" style={{ transformOrigin: '0 0 0' }}>
|
||||
<Paper
|
||||
className="rounded-8"
|
||||
onMouseEnter={() => handleToggle(true)}
|
||||
onMouseLeave={() => handleToggle(false)}
|
||||
>
|
||||
{item.children && (
|
||||
<ul className={clsx('popper-navigation-list', dense && 'dense', 'px-0')}>
|
||||
{item.children.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`horizontal-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={nestedLevel}
|
||||
dense={dense}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Paper>
|
||||
</Grow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Popper>,
|
||||
document.querySelector('#root')
|
||||
)}
|
||||
</Manager>
|
||||
);
|
||||
}, [dense, handleToggle, item, nestedLevel, opened, props.location.pathname, theme.direction]);
|
||||
}
|
||||
|
||||
FuseNavHorizontalGroup.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavHorizontalGroup.defaultProps = {};
|
||||
|
||||
const NavHorizontalGroup = withRouter(memo(FuseNavHorizontalGroup));
|
||||
|
||||
export default NavHorizontalGroup;
|
||||
@@ -0,0 +1,84 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo, useMemo } from 'react';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: 'none!important',
|
||||
minHeight: 48,
|
||||
'&.active': {
|
||||
backgroundColor: `${theme.palette.secondary.main}!important`,
|
||||
color: `${theme.palette.secondary.contrastText}!important`,
|
||||
pointerEvents: 'none',
|
||||
'& .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-icon': {},
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 16px',
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseNavHorizontalItem(props) {
|
||||
const { item } = props;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<StyledListItem
|
||||
button
|
||||
component={NavLinkAdapter}
|
||||
to={item.url || ''}
|
||||
activeClassName={item.url ? 'active' : ''}
|
||||
className={clsx('fuse-list-item', item.active && 'active')}
|
||||
end={item.end}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
classes={{ primary: 'text-13 fuse-list-item-text-primary truncate' }}
|
||||
/>
|
||||
|
||||
{item.badge && <FuseNavBadge className="ltr:ml-8 rtl:mr-8" badge={item.badge} />}
|
||||
</StyledListItem>
|
||||
),
|
||||
[item.badge, item.exact, item.icon, item.iconClass, item.title, item.url]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavHorizontalItem.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavHorizontalItem.defaultProps = {};
|
||||
|
||||
const NavHorizontalItem = withRouter(memo(FuseNavHorizontalItem));
|
||||
|
||||
export default NavHorizontalItem;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { memo, useMemo } from 'react';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: 'none!important',
|
||||
minHeight: 48,
|
||||
'&.active': {
|
||||
backgroundColor: `${theme.palette.secondary.main}!important`,
|
||||
color: `${theme.palette.secondary.contrastText}!important`,
|
||||
pointerEvents: 'none',
|
||||
'& .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-icon': {},
|
||||
'& .fuse-list-item-text': {
|
||||
padding: '0 0 0 16px',
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseNavHorizontalLink(props) {
|
||||
const { item } = props;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<StyledListItem
|
||||
button
|
||||
component="a"
|
||||
href={item.url}
|
||||
target={item.target ? item.target : '_blank'}
|
||||
className={clsx('fuse-list-item')}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
classes={{ primary: 'text-13 fuse-list-item-text-primary truncate' }}
|
||||
/>
|
||||
|
||||
{item.badge && <FuseNavBadge className="ltr:ml-8 rtl:mr-8" badge={item.badge} />}
|
||||
</StyledListItem>
|
||||
),
|
||||
[item.badge, item.icon, item.iconClass, item.target, item.title, item.url]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavHorizontalLink.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavHorizontalLink.defaultProps = {};
|
||||
|
||||
const NavHorizontalLink = withRouter(memo(FuseNavHorizontalLink));
|
||||
|
||||
export default NavHorizontalLink;
|
||||
1
src/@fuse/core/FuseNavigation/index.js
Normal file
1
src/@fuse/core/FuseNavigation/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseNavigation';
|
||||
@@ -0,0 +1,69 @@
|
||||
import List from '@mui/material/List';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import FuseNavItem from '../FuseNavItem';
|
||||
|
||||
const StyledList = styled(List)(({ theme }) => ({
|
||||
'& .fuse-list-item': {
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
|
||||
},
|
||||
'&:focus:not(.active)': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-text': {
|
||||
margin: 0,
|
||||
},
|
||||
'& .fuse-list-item-text-primary': {
|
||||
lineHeight: '20px',
|
||||
},
|
||||
'&.active-square-list': {
|
||||
'& .fuse-list-item, & .active.fuse-list-item': {
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
},
|
||||
},
|
||||
'&.dense': {
|
||||
'& .fuse-list-item': {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
height: 32,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseNavVerticalLayout1(props) {
|
||||
const { navigation, layout, active, dense, className, onItemClick } = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
function handleItemClick(item) {
|
||||
onItemClick?.(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledList
|
||||
className={clsx(
|
||||
'navigation whitespace-nowrap px-12 py-0',
|
||||
`active-${active}-list`,
|
||||
dense && 'dense',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{navigation.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`vertical-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={0}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
}
|
||||
|
||||
export default FuseNavVerticalLayout1;
|
||||
@@ -0,0 +1,63 @@
|
||||
import List from '@mui/material/List';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import FuseNavVerticalTab from './types/FuseNavVerticalTab';
|
||||
|
||||
const StyledList = styled(List)(({ theme }) => ({
|
||||
'& .fuse-list-item': {
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0,0,0,.04)',
|
||||
},
|
||||
'&:focus:not(.active)': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0,0,0,.05)',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-text-primary': {
|
||||
lineHeight: '1',
|
||||
},
|
||||
'&.active-square-list': {
|
||||
'& .fuse-list-item, & .active.fuse-list-item': {
|
||||
width: '100%',
|
||||
borderRadius: '0',
|
||||
},
|
||||
},
|
||||
'&.dense': {},
|
||||
}));
|
||||
|
||||
function FuseNavVerticalLayout2(props) {
|
||||
const { navigation, layout, active, dense, className, onItemClick, firstLevel, selectedId } =
|
||||
props;
|
||||
const theme = useTheme();
|
||||
|
||||
function handleItemClick(item) {
|
||||
onItemClick?.(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledList
|
||||
className={clsx(
|
||||
'navigation whitespace-nowrap items-center flex flex-col',
|
||||
`active-${active}-list`,
|
||||
dense && 'dense',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{navigation.map((_item) => (
|
||||
<FuseNavVerticalTab
|
||||
key={_item.id}
|
||||
type={`vertical-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={0}
|
||||
onItemClick={handleItemClick}
|
||||
firstLevel={firstLevel}
|
||||
dense={dense}
|
||||
selectedId={selectedId}
|
||||
/>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
}
|
||||
|
||||
export default FuseNavVerticalLayout2;
|
||||
@@ -0,0 +1,168 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import List from '@mui/material/List';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseNavItem from '../../FuseNavItem';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const Root = styled(List)(({ theme, ...props }) => ({
|
||||
padding: 0,
|
||||
'&.open': {},
|
||||
'& > .fuse-list-item': {
|
||||
minHeight: 44,
|
||||
width: '100%',
|
||||
borderRadius: '6px',
|
||||
margin: '0 0 4px 0',
|
||||
paddingRight: 16,
|
||||
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
color: alpha(theme.palette.text.primary, 0.7),
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& > .fuse-list-item-icon': {
|
||||
marginRight: 16,
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function needsToBeOpened(location, item) {
|
||||
return location && isUrlInChildren(item, location.pathname);
|
||||
}
|
||||
|
||||
function isUrlInChildren(parent, url) {
|
||||
if (!parent.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parent.children.length; i += 1) {
|
||||
if (parent.children[i].children) {
|
||||
if (isUrlInChildren(parent.children[i], url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function FuseNavVerticalCollapse(props) {
|
||||
const [open, setOpen] = useState(() => needsToBeOpened(props.location, props.item));
|
||||
const { item, nestedLevel, onItemClick } = props;
|
||||
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (needsToBeOpened(location, props.item)) {
|
||||
if (!open) {
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [location, props.item]);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Root className={clsx(open && 'open')} itempadding={itempadding} sx={item.sx}>
|
||||
<ListItem
|
||||
component={item.url ? NavLinkAdapter : 'li'}
|
||||
button
|
||||
className="fuse-list-item"
|
||||
onClick={() => setOpen(!open)}
|
||||
to={item.url}
|
||||
end={item.end}
|
||||
role="button"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
secondary={item.subtitle}
|
||||
classes={{
|
||||
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
|
||||
secondary:
|
||||
'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.badge && <FuseNavBadge className="mx-4" badge={item.badge} />}
|
||||
|
||||
<IconButton
|
||||
disableRipple
|
||||
className="w-20 h-20 -mx-12 p-0 focus:bg-transparent hover:bg-transparent"
|
||||
onClick={(ev) => ev.preventDefault()}
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon size={16} className="arrow-icon" color="inherit">
|
||||
{open ? 'heroicons-solid:chevron-down' : 'heroicons-solid:chevron-right'}
|
||||
</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
|
||||
{item.children && (
|
||||
<Collapse in={open} className="collapse-children">
|
||||
{item.children.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`vertical-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={nestedLevel + 1}
|
||||
onItemClick={onItemClick}
|
||||
/>
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
</Root>
|
||||
),
|
||||
[
|
||||
item.badge,
|
||||
item.children,
|
||||
item.icon,
|
||||
item.iconClass,
|
||||
item.title,
|
||||
item.url,
|
||||
itempadding,
|
||||
nestedLevel,
|
||||
onItemClick,
|
||||
open,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavVerticalCollapse.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
FuseNavVerticalCollapse.defaultProps = {};
|
||||
|
||||
const NavVerticalCollapse = FuseNavVerticalCollapse;
|
||||
|
||||
export default NavVerticalCollapse;
|
||||
@@ -0,0 +1,102 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMemo } from 'react';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import { ListItemText } from '@mui/material';
|
||||
import FuseNavItem from '../../FuseNavItem';
|
||||
|
||||
const Root = styled(ListItem)(({ theme, itempadding, ...props }) => ({
|
||||
minminHeight: 44,
|
||||
width: '100%',
|
||||
borderRadius: '6px',
|
||||
margin: '28px 0 0 0',
|
||||
paddingRight: 16,
|
||||
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
color: alpha(theme.palette.text.primary, 0.7),
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.025em',
|
||||
}));
|
||||
|
||||
function FuseNavVerticalGroup(props) {
|
||||
const { item, nestedLevel, onItemClick } = props;
|
||||
|
||||
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Root
|
||||
component={item.url ? NavLinkAdapter : 'li'}
|
||||
itempadding={itempadding}
|
||||
className={clsx(
|
||||
'fuse-list-subheader flex items-center py-10',
|
||||
!item.url && 'cursor-default'
|
||||
)}
|
||||
onClick={() => onItemClick && onItemClick(item)}
|
||||
to={item.url}
|
||||
end={item.end}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<ListItemText
|
||||
className="fuse-list-subheader-text"
|
||||
sx={{
|
||||
margin: 0,
|
||||
'& > .MuiListItemText-primary': {
|
||||
fontSize: 12,
|
||||
color: 'secondary.light',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '.05em',
|
||||
lineHeight: '20px',
|
||||
},
|
||||
|
||||
'& > .MuiListItemText-secondary': {
|
||||
fontSize: 11,
|
||||
color: 'text.disabled',
|
||||
letterSpacing: '.06px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
}}
|
||||
primary={item.title}
|
||||
secondary={item.subtitle}
|
||||
/>
|
||||
</Root>
|
||||
{item.children && (
|
||||
<>
|
||||
{item.children.map((_item) => (
|
||||
<FuseNavItem
|
||||
key={_item.id}
|
||||
type={`vertical-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={nestedLevel}
|
||||
onItemClick={onItemClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[item, itempadding, nestedLevel, onItemClick]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavVerticalGroup.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavVerticalGroup.defaultProps = {};
|
||||
|
||||
const NavVerticalGroup = FuseNavVerticalGroup;
|
||||
|
||||
export default NavVerticalGroup;
|
||||
@@ -0,0 +1,106 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMemo } from 'react';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const Root = styled(ListItem)(({ theme, ...props }) => ({
|
||||
minHeight: 44,
|
||||
width: '100%',
|
||||
borderRadius: '6px',
|
||||
margin: '0 0 4px 0',
|
||||
paddingRight: 16,
|
||||
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
color: alpha(theme.palette.text.primary, 0.7),
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none!important',
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'&.active': {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light'
|
||||
? 'rgba(0, 0, 0, .05)!important'
|
||||
: 'rgba(255, 255, 255, .1)!important',
|
||||
pointerEvents: 'none',
|
||||
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
|
||||
'& > .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& > .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& >.fuse-list-item-icon': {
|
||||
marginRight: 16,
|
||||
color: 'inherit',
|
||||
},
|
||||
'& > .fuse-list-item-text': {},
|
||||
}));
|
||||
|
||||
function FuseNavVerticalItem(props) {
|
||||
const { item, nestedLevel, onItemClick } = props;
|
||||
|
||||
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Root
|
||||
button
|
||||
component={NavLinkAdapter}
|
||||
to={item.url || ''}
|
||||
activeClassName={item.url ? 'active' : ''}
|
||||
className={clsx('fuse-list-item', item.active && 'active')}
|
||||
onClick={() => onItemClick && onItemClick(item)}
|
||||
end={item.end}
|
||||
itempadding={itempadding}
|
||||
role="button"
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
secondary={item.subtitle}
|
||||
classes={{
|
||||
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
|
||||
secondary: 'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
|
||||
}}
|
||||
/>
|
||||
{item.badge && <FuseNavBadge badge={item.badge} />}
|
||||
</Root>
|
||||
),
|
||||
[item, itempadding, onItemClick]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavVerticalItem.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavVerticalItem.defaultProps = {};
|
||||
|
||||
const NavVerticalItem = FuseNavVerticalItem;
|
||||
|
||||
export default NavVerticalItem;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const Root = styled(ListItem)(({ theme, ...props }) => ({
|
||||
minHeight: 44,
|
||||
width: '100%',
|
||||
borderRadius: '6px',
|
||||
margin: '0 0 4px 0',
|
||||
paddingRight: 16,
|
||||
paddingLeft: props.itempadding > 80 ? 80 : props.itempadding,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
'&.active': {
|
||||
backgroundColor: `${theme.palette.secondary.main}!important`,
|
||||
color: `${theme.palette.secondary.contrastText}!important`,
|
||||
pointerEvents: 'none',
|
||||
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
|
||||
'& > .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& > .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& > .fuse-list-item-icon': {
|
||||
marginRight: 16,
|
||||
},
|
||||
'& > .fuse-list-item-text': {},
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: 'none!important',
|
||||
}));
|
||||
|
||||
function FuseNavVerticalLink(props) {
|
||||
const dispatch = useDispatch();
|
||||
const { item, nestedLevel, onItemClick } = props;
|
||||
|
||||
const itempadding = nestedLevel > 0 ? 38 + nestedLevel * 16 : 16;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Root
|
||||
button
|
||||
component="a"
|
||||
href={item.url}
|
||||
target={item.target ? item.target : '_blank'}
|
||||
className="fuse-list-item"
|
||||
onClick={() => onItemClick && onItemClick(item)}
|
||||
role="button"
|
||||
itempadding={itempadding}
|
||||
sx={item.sx}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon shrink-0', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
)}
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text"
|
||||
primary={item.title}
|
||||
secondary={item.subtitle}
|
||||
classes={{
|
||||
primary: 'text-13 font-medium fuse-list-item-text-primary truncate',
|
||||
secondary: 'text-11 font-medium fuse-list-item-text-secondary leading-normal truncate',
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.badge && <FuseNavBadge badge={item.badge} />}
|
||||
</Root>
|
||||
),
|
||||
[item, itempadding, onItemClick]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavVerticalLink.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
FuseNavVerticalLink.defaultProps = {};
|
||||
|
||||
const NavVerticalLink = FuseNavVerticalLink;
|
||||
|
||||
export default NavVerticalLink;
|
||||
@@ -0,0 +1,177 @@
|
||||
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Box } from '@mui/system';
|
||||
import FuseNavBadge from '../../FuseNavBadge';
|
||||
import FuseSvgIcon from '../../../FuseSvgIcon';
|
||||
|
||||
const Root = styled(Box)(({ theme }) => ({
|
||||
'& > .fuse-list-item': {
|
||||
minHeight: 100,
|
||||
height: 100,
|
||||
width: 100,
|
||||
borderRadius: 12,
|
||||
margin: '0 0 4px 0',
|
||||
color: alpha(theme.palette.text.primary, 0.7),
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none!important',
|
||||
padding: 0,
|
||||
'&.dense': {
|
||||
minHeight: 52,
|
||||
height: 52,
|
||||
width: 52,
|
||||
},
|
||||
'&.type-divider': {
|
||||
padding: 0,
|
||||
height: 2,
|
||||
minHeight: 2,
|
||||
margin: '12px 0',
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light'
|
||||
? 'rgba(0, 0, 0, .05)!important'
|
||||
: 'rgba(255, 255, 255, .1)!important',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'&.active': {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light'
|
||||
? 'rgba(0, 0, 0, .05)!important'
|
||||
: 'rgba(255, 255, 255, .1)!important',
|
||||
// pointerEvents: 'none',
|
||||
transition: 'border-radius .15s cubic-bezier(0.4,0.0,0.2,1)',
|
||||
'& .fuse-list-item-text-primary': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
'& .fuse-list-item-icon': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .fuse-list-item-text': {},
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseNavVerticalTab(props) {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
|
||||
const { item, onItemClick, firstLevel, dense, selectedId } = props;
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Root sx={item.sx}>
|
||||
<ListItem
|
||||
button
|
||||
component={item.url && NavLinkAdapter}
|
||||
to={item.url}
|
||||
end={item.end}
|
||||
className={clsx(
|
||||
`type-${item.type}`,
|
||||
dense && 'dense',
|
||||
selectedId === item.id && 'active',
|
||||
'fuse-list-item flex flex-col items-center justify-center p-12'
|
||||
)}
|
||||
onClick={() => onItemClick && onItemClick(item)}
|
||||
role="button"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{dense ? (
|
||||
<Tooltip title={item.title || ''} placement="right">
|
||||
<div className="w-32 h-32 min-h-32 flex items-center justify-center relative">
|
||||
{item.icon ? (
|
||||
<FuseSvgIcon
|
||||
className={clsx('fuse-list-item-icon', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
) : (
|
||||
item.title && <div className="font-bold text-16">{item.title[0]}</div>
|
||||
)}
|
||||
{item.badge && (
|
||||
<FuseNavBadge
|
||||
badge={item.badge}
|
||||
className="absolute top-0 ltr:right-0 rtl:left-0 min-w-16 h-16 p-4 justify-center"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-32 h-32 min-h-32 flex items-center justify-center relative mb-8">
|
||||
{item.icon ? (
|
||||
<FuseSvgIcon
|
||||
size={32}
|
||||
className={clsx('fuse-list-item-icon', item.iconClass)}
|
||||
color="action"
|
||||
>
|
||||
{item.icon}
|
||||
</FuseSvgIcon>
|
||||
) : (
|
||||
item.title && <div className="font-bold text-20">{item.title[0]}</div>
|
||||
)}
|
||||
{item.badge && (
|
||||
<FuseNavBadge
|
||||
badge={item.badge}
|
||||
className="absolute top-0 ltr:right-0 rtl:left-0 min-w-16 h-16 p-4 justify-center"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ListItemText
|
||||
className="fuse-list-item-text grow-0 w-full"
|
||||
primary={item.title}
|
||||
classes={{
|
||||
primary:
|
||||
'text-12 font-medium fuse-list-item-text-primary truncate text-center truncate',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ListItem>
|
||||
{!firstLevel &&
|
||||
item.children &&
|
||||
item.children.map((_item) => (
|
||||
<NavVerticalTab
|
||||
key={_item.id}
|
||||
type={`vertical-${_item.type}`}
|
||||
item={_item}
|
||||
nestedLevel={0}
|
||||
onItemClick={onItemClick}
|
||||
dense={dense}
|
||||
selectedId={selectedId}
|
||||
/>
|
||||
))}
|
||||
</Root>
|
||||
),
|
||||
[firstLevel, item, onItemClick, dense, selectedId]
|
||||
);
|
||||
}
|
||||
|
||||
FuseNavVerticalTab.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
FuseNavVerticalTab.defaultProps = {};
|
||||
|
||||
const NavVerticalTab = FuseNavVerticalTab;
|
||||
|
||||
export default NavVerticalTab;
|
||||
281
src/@fuse/core/FusePageCarded/FusePageCarded.js
Normal file
281
src/@fuse/core/FusePageCarded/FusePageCarded.js
Normal file
@@ -0,0 +1,281 @@
|
||||
import FuseScrollbars from '@fuse/core/FuseScrollbars';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { forwardRef, memo, useImperativeHandle, useRef } from 'react';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import FusePageCardedHeader from './FusePageCardedHeader';
|
||||
import FusePageCardedSidebar from './FusePageCardedSidebar';
|
||||
|
||||
const Root = styled('div')(({ theme, ...props }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
minHeight: '100%',
|
||||
position: 'relative',
|
||||
flex: '1 1 auto',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
|
||||
'& .FusePageCarded-scroll-content': {
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-wrapper': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: '1 1 auto',
|
||||
zIndex: 2,
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
...(props.scroll === 'content' && {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
},
|
||||
|
||||
'& .FusePageCarded-header': {
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-contentWrapper': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 auto',
|
||||
overflow: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
zIndex: 9999,
|
||||
},
|
||||
|
||||
'& .FusePageCarded-toolbar': {
|
||||
height: toolbarHeight,
|
||||
minHeight: toolbarHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-content': {
|
||||
flex: '1 0 auto',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-sidebarWrapper': {
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
'&.permanent': {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
position: 'relative',
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
'&.closed': {
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
|
||||
'&.FusePageCarded-leftSidebar': {
|
||||
marginLeft: -props.leftsidebarwidth,
|
||||
},
|
||||
'&.FusePageCarded-rightSidebar': {
|
||||
marginRight: -props.rightsidebarwidth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageCarded-sidebar': {
|
||||
position: 'absolute',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
|
||||
'&.permanent': {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
position: 'relative',
|
||||
},
|
||||
},
|
||||
maxWidth: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-leftSidebar': {
|
||||
width: props.leftsidebarwidth,
|
||||
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
// borderRight: `1px solid ${theme.palette.divider}`,
|
||||
// borderLeft: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageCarded-rightSidebar': {
|
||||
width: props.rightsidebarwidth,
|
||||
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
// borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
// borderRight: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageCarded-sidebarHeader': {
|
||||
height: headerHeight,
|
||||
minHeight: headerHeight,
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
|
||||
'& .FusePageCarded-sidebarHeaderInnerSidebar': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
height: 'auto',
|
||||
minHeight: 'auto',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-sidebarContent': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageCarded-backdrop': {
|
||||
position: 'absolute',
|
||||
},
|
||||
}));
|
||||
|
||||
const headerHeight = 120;
|
||||
const toolbarHeight = 64;
|
||||
|
||||
const FusePageCarded = forwardRef((props, ref) => {
|
||||
// console.info("render::FusePageCarded");
|
||||
const leftSidebarRef = useRef(null);
|
||||
const rightSidebarRef = useRef(null);
|
||||
const rootRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
rootRef,
|
||||
toggleLeftSidebar: (val) => {
|
||||
leftSidebarRef.current.toggleSidebar(val);
|
||||
},
|
||||
toggleRightSidebar: (val) => {
|
||||
rightSidebarRef.current.toggleSidebar(val);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyles
|
||||
styles={(theme) => ({
|
||||
...(props.scroll !== 'page' && {
|
||||
'#fuse-toolbar': {
|
||||
position: 'static',
|
||||
},
|
||||
'#fuse-footer': {
|
||||
position: 'static',
|
||||
},
|
||||
}),
|
||||
...(props.scroll === 'page' && {
|
||||
'#fuse-toolbar': {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
},
|
||||
'#fuse-footer': {
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
},
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
<Root
|
||||
className={clsx(
|
||||
'FusePageCarded-root',
|
||||
`FusePageCarded-scroll-${props.scroll}`,
|
||||
props.className
|
||||
)}
|
||||
ref={rootRef}
|
||||
scroll={props.scroll}
|
||||
leftsidebarwidth={props.leftSidebarWidth}
|
||||
rightsidebarwidth={props.rightSidebarWidth}
|
||||
>
|
||||
{props.header && <FusePageCardedHeader header={props.header} />}
|
||||
|
||||
<div className="flex flex-auto flex-col container z-10 h-full shadow-1 rounded-t-16 relative overflow-hidden">
|
||||
<div className="FusePageCarded-wrapper">
|
||||
{props.leftSidebarContent && (
|
||||
<FusePageCardedSidebar
|
||||
position="left"
|
||||
content={props.leftSidebarContent}
|
||||
variant={props.leftSidebarVariant || 'permanent'}
|
||||
ref={leftSidebarRef}
|
||||
rootRef={rootRef}
|
||||
open={props.leftSidebarOpen}
|
||||
onClose={props.leftSidebarOnClose}
|
||||
sidebarWidth={props.leftSidebarWidth}
|
||||
/>
|
||||
)}
|
||||
<FuseScrollbars
|
||||
className="FusePageCarded-contentWrapper"
|
||||
enable={props.scroll === 'content'}
|
||||
>
|
||||
{props.content && (
|
||||
<div className={clsx('FusePageCarded-content')}>{props.content}</div>
|
||||
)}
|
||||
</FuseScrollbars>
|
||||
{props.rightSidebarContent && (
|
||||
<FusePageCardedSidebar
|
||||
position="right"
|
||||
content={props.rightSidebarContent}
|
||||
variant={props.rightSidebarVariant || 'permanent'}
|
||||
ref={rightSidebarRef}
|
||||
rootRef={rootRef}
|
||||
open={props.rightSidebarOpen}
|
||||
onClose={props.rightSidebarOnClose}
|
||||
sidebarWidth={props.rightSidebarWidth}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Root>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FusePageCarded.propTypes = {
|
||||
leftSidebarHeader: PropTypes.node,
|
||||
leftSidebarContent: PropTypes.node,
|
||||
leftSidebarVariant: PropTypes.node,
|
||||
rightSidebarContent: PropTypes.node,
|
||||
rightSidebarVariant: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
content: PropTypes.node,
|
||||
contentToolbar: PropTypes.node,
|
||||
scroll: PropTypes.oneOf(['normal', 'page', 'content']),
|
||||
leftSidebarOpen: PropTypes.bool,
|
||||
rightSidebarOpen: PropTypes.bool,
|
||||
leftSidebarWidth: PropTypes.number,
|
||||
rightSidebarWidth: PropTypes.number,
|
||||
rightSidebarOnClose: PropTypes.func,
|
||||
leftSidebarOnClose: PropTypes.func,
|
||||
};
|
||||
|
||||
FusePageCarded.defaultProps = {
|
||||
classes: {},
|
||||
scroll: 'page',
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false,
|
||||
rightSidebarWidth: 240,
|
||||
leftSidebarWidth: 240,
|
||||
};
|
||||
|
||||
export default memo(styled(FusePageCarded)``);
|
||||
9
src/@fuse/core/FusePageCarded/FusePageCardedHeader.js
Normal file
9
src/@fuse/core/FusePageCarded/FusePageCardedHeader.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FusePageCardedHeader(props) {
|
||||
return (
|
||||
<div className={clsx('FusePageCarded-header', 'container')}>{props.header && props.header}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FusePageCardedHeader;
|
||||
92
src/@fuse/core/FusePageCarded/FusePageCardedSidebar.js
Normal file
92
src/@fuse/core/FusePageCarded/FusePageCardedSidebar.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Hidden from '@mui/material/Hidden';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import FusePageCardedSidebarContent from './FusePageCardedSidebarContent';
|
||||
|
||||
const FusePageCardedSidebar = forwardRef((props, ref) => {
|
||||
const { open, position, variant, rootRef, sidebarWidth } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(open);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
toggleSidebar: handleToggleDrawer,
|
||||
}));
|
||||
|
||||
const handleToggleDrawer = useCallback((val) => {
|
||||
setIsOpen(val);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleToggleDrawer(open);
|
||||
}, [handleToggleDrawer, open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hidden lgUp={variant === 'permanent'}>
|
||||
<SwipeableDrawer
|
||||
variant="temporary"
|
||||
anchor={position}
|
||||
open={isOpen}
|
||||
onOpen={(ev) => {}}
|
||||
onClose={() => props?.onClose()}
|
||||
disableSwipeToOpen
|
||||
classes={{
|
||||
root: clsx('FusePageCarded-sidebarWrapper', variant),
|
||||
paper: clsx(
|
||||
'FusePageCarded-sidebar',
|
||||
variant,
|
||||
position === 'left' ? 'FusePageCarded-leftSidebar' : 'FusePageCarded-rightSidebar'
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
width: sidebarWidth,
|
||||
maxWidth: '100%',
|
||||
},
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
// container={rootRef.current}
|
||||
BackdropProps={{
|
||||
classes: {
|
||||
root: 'FusePageCarded-backdrop',
|
||||
},
|
||||
}}
|
||||
style={{ position: 'absolute' }}
|
||||
>
|
||||
<FusePageCardedSidebarContent {...props} />
|
||||
</SwipeableDrawer>
|
||||
</Hidden>
|
||||
{variant === 'permanent' && (
|
||||
<Hidden lgDown>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
anchor={position}
|
||||
className={clsx(
|
||||
'FusePageCarded-sidebarWrapper',
|
||||
variant,
|
||||
isOpen ? 'opened' : 'closed',
|
||||
position === 'left' ? 'FusePageCarded-leftSidebar' : 'FusePageCarded-rightSidebar'
|
||||
)}
|
||||
open={isOpen}
|
||||
onClose={props?.onClose}
|
||||
classes={{
|
||||
paper: clsx('FusePageCarded-sidebar', variant),
|
||||
}}
|
||||
>
|
||||
<FusePageCardedSidebarContent {...props} />
|
||||
</Drawer>
|
||||
</Hidden>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FusePageCardedSidebar.defaultProps = {
|
||||
open: true,
|
||||
};
|
||||
|
||||
export default FusePageCardedSidebar;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import FuseScrollbars from '@fuse/core/FuseScrollbars';
|
||||
import { ThemeProvider, useTheme } from '@mui/material/styles';
|
||||
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FusePageCardedSidebarContent(props) {
|
||||
const theme = useTheme();
|
||||
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
|
||||
|
||||
return (
|
||||
<FuseScrollbars enable={props.innerScroll}>
|
||||
{props.header && (
|
||||
<ThemeProvider theme={contrastTheme}>
|
||||
<div
|
||||
className={clsx(
|
||||
'FusePageCarded-sidebarHeader',
|
||||
props.variant,
|
||||
props.sidebarInner && 'FusePageCarded-sidebarHeaderInnerSidebar'
|
||||
)}
|
||||
>
|
||||
{props.header}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
)}
|
||||
|
||||
{props.content && <div className="FusePageCarded-sidebarContent">{props.content}</div>}
|
||||
</FuseScrollbars>
|
||||
);
|
||||
}
|
||||
|
||||
export default FusePageCardedSidebarContent;
|
||||
1
src/@fuse/core/FusePageCarded/index.js
Normal file
1
src/@fuse/core/FusePageCarded/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FusePageCarded';
|
||||
299
src/@fuse/core/FusePageSimple/FusePageSimple.js
Normal file
299
src/@fuse/core/FusePageSimple/FusePageSimple.js
Normal file
@@ -0,0 +1,299 @@
|
||||
import FuseScrollbars from '@fuse/core/FuseScrollbars';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import clsx from 'clsx';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { forwardRef, memo, useImperativeHandle, useRef } from 'react';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import FusePageSimpleHeader from './FusePageSimpleHeader';
|
||||
import FusePageSimpleSidebar from './FusePageSimpleSidebar';
|
||||
|
||||
const Root = styled('div')(({ theme, ...props }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
minHeight: '100%',
|
||||
position: 'relative',
|
||||
flex: '1 1 auto',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
|
||||
'&.FusePageSimple-scroll-content': {
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-wrapper': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: '1 1 auto',
|
||||
zIndex: 2,
|
||||
minWidth: 0,
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
|
||||
...(props.scroll === 'content' && {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
},
|
||||
|
||||
'& .FusePageSimple-header': {
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
backgroundSize: 'cover',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-topBg': {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: headerHeight,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-contentWrapper': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
flex: '1 1 auto',
|
||||
overflow: 'hidden',
|
||||
// WebkitOverflowScrolling: 'touch',
|
||||
zIndex: 9999,
|
||||
},
|
||||
|
||||
'& .FusePageSimple-toolbar': {
|
||||
height: toolbarHeight,
|
||||
minHeight: toolbarHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-content': {
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
alignItems: 'start',
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-sidebarWrapper': {
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
'&.permanent': {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
position: 'relative',
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
'&.closed': {
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
|
||||
'&.FusePageSimple-leftSidebar': {
|
||||
marginLeft: -props.leftsidebarwidth,
|
||||
},
|
||||
'&.FusePageSimple-rightSidebar': {
|
||||
marginRight: -props.rightsidebarwidth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageSimple-sidebar': {
|
||||
position: 'absolute',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
|
||||
'&.permanent': {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
position: 'relative',
|
||||
},
|
||||
},
|
||||
maxWidth: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-leftSidebar': {
|
||||
width: props.leftsidebarwidth,
|
||||
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
borderLeft: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageSimple-rightSidebar': {
|
||||
width: props.rightsidebarwidth,
|
||||
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
borderRight: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'& .FusePageSimple-sidebarHeader': {
|
||||
height: headerHeight,
|
||||
minHeight: headerHeight,
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
|
||||
'& .FusePageSimple-sidebarHeaderInnerSidebar': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
height: 'auto',
|
||||
minHeight: 'auto',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-sidebarContent': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100%',
|
||||
},
|
||||
|
||||
'& .FusePageSimple-backdrop': {
|
||||
position: 'absolute',
|
||||
},
|
||||
}));
|
||||
|
||||
const headerHeight = 120;
|
||||
const toolbarHeight = 64;
|
||||
|
||||
const FusePageSimple = forwardRef((props, ref) => {
|
||||
// console.info("render::FusePageSimple");
|
||||
const leftSidebarRef = useRef(null);
|
||||
const rightSidebarRef = useRef(null);
|
||||
const rootRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
rootRef,
|
||||
toggleLeftSidebar: (val) => {
|
||||
leftSidebarRef.current.toggleSidebar(val);
|
||||
},
|
||||
toggleRightSidebar: (val) => {
|
||||
rightSidebarRef.current.toggleSidebar(val);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyles
|
||||
styles={(theme) => ({
|
||||
...(props.scroll !== 'page' && {
|
||||
'#fuse-toolbar': {
|
||||
position: 'static',
|
||||
},
|
||||
'#fuse-footer': {
|
||||
position: 'static',
|
||||
},
|
||||
}),
|
||||
...(props.scroll === 'page' && {
|
||||
'#fuse-toolbar': {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
},
|
||||
'#fuse-footer': {
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
},
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
<Root
|
||||
className={clsx(
|
||||
'FusePageSimple-root',
|
||||
`FusePageSimple-scroll-${props.scroll}`,
|
||||
props.className
|
||||
)}
|
||||
ref={rootRef}
|
||||
scroll={props.scroll}
|
||||
leftsidebarwidth={props.leftSidebarWidth}
|
||||
rightsidebarwidth={props.rightSidebarWidth}
|
||||
>
|
||||
<div className="flex flex-auto flex-col z-10 h-full">
|
||||
<div className="FusePageSimple-wrapper">
|
||||
{props.leftSidebarContent && (
|
||||
<FusePageSimpleSidebar
|
||||
position="left"
|
||||
content={props.leftSidebarContent}
|
||||
variant={props.leftSidebarVariant || 'permanent'}
|
||||
ref={leftSidebarRef}
|
||||
rootRef={rootRef}
|
||||
open={props.leftSidebarOpen}
|
||||
onClose={props.leftSidebarOnClose}
|
||||
sidebarWidth={props.leftSidebarWidth}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="FusePageSimple-contentWrapper"
|
||||
// enable={props.scroll === 'page'}
|
||||
>
|
||||
{props.header && <FusePageSimpleHeader header={props.header} />}
|
||||
{props.content && (
|
||||
<FuseScrollbars
|
||||
enable={props.scroll === 'content'}
|
||||
className={clsx('FusePageSimple-content container')}
|
||||
>
|
||||
{props.content}
|
||||
</FuseScrollbars>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.rightSidebarContent && (
|
||||
<FusePageSimpleSidebar
|
||||
position="right"
|
||||
content={props.rightSidebarContent}
|
||||
variant={props.rightSidebarVariant || 'permanent'}
|
||||
ref={rightSidebarRef}
|
||||
rootRef={rootRef}
|
||||
open={props.rightSidebarOpen}
|
||||
onClose={props.rightSidebarOnClose}
|
||||
sidebarWidth={props.rightSidebarWidth}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Root>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FusePageSimple.propTypes = {
|
||||
leftSidebarContent: PropTypes.node,
|
||||
leftSidebarVariant: PropTypes.node,
|
||||
rightSidebarContent: PropTypes.node,
|
||||
rightSidebarVariant: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
content: PropTypes.node,
|
||||
scroll: PropTypes.oneOf(['normal', 'page', 'content']),
|
||||
leftSidebarOpen: PropTypes.bool,
|
||||
rightSidebarOpen: PropTypes.bool,
|
||||
leftSidebarWidth: PropTypes.number,
|
||||
rightSidebarWidth: PropTypes.number,
|
||||
rightSidebarOnClose: PropTypes.func,
|
||||
leftSidebarOnClose: PropTypes.func,
|
||||
};
|
||||
|
||||
FusePageSimple.defaultProps = {
|
||||
classes: {},
|
||||
scroll: 'page',
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false,
|
||||
rightSidebarWidth: 240,
|
||||
leftSidebarWidth: 240,
|
||||
};
|
||||
|
||||
export default memo(styled(FusePageSimple)``);
|
||||
11
src/@fuse/core/FusePageSimple/FusePageSimpleHeader.js
Normal file
11
src/@fuse/core/FusePageSimple/FusePageSimpleHeader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FusePageSimpleHeader(props) {
|
||||
return (
|
||||
<div className={clsx('FusePageSimple-header', props.className)}>
|
||||
<div className="container">{props.header && props.header}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FusePageSimpleHeader;
|
||||
93
src/@fuse/core/FusePageSimple/FusePageSimpleSidebar.js
Normal file
93
src/@fuse/core/FusePageSimple/FusePageSimpleSidebar.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Hidden from '@mui/material/Hidden';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import FusePageSimpleSidebarContent from './FusePageSimpleSidebarContent';
|
||||
|
||||
const FusePageSimpleSidebar = forwardRef((props, ref) => {
|
||||
const { open, position, variant, rootRef, sidebarWidth } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(open);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
toggleSidebar: handleToggleDrawer,
|
||||
}));
|
||||
|
||||
const handleToggleDrawer = useCallback((val) => {
|
||||
setIsOpen(val);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleToggleDrawer(open);
|
||||
}, [handleToggleDrawer, open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hidden lgUp={variant === 'permanent'}>
|
||||
<SwipeableDrawer
|
||||
variant="temporary"
|
||||
anchor={position}
|
||||
open={isOpen}
|
||||
onOpen={(ev) => {}}
|
||||
onClose={() => props?.onClose()}
|
||||
disableSwipeToOpen
|
||||
classes={{
|
||||
root: clsx('FusePageSimple-sidebarWrapper', variant),
|
||||
paper: clsx(
|
||||
'FusePageSimple-sidebar',
|
||||
variant,
|
||||
position === 'left' ? 'FusePageSimple-leftSidebar' : 'FusePageSimple-rightSidebar'
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
width: sidebarWidth,
|
||||
maxWidth: '100%',
|
||||
},
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
// container={rootRef.current}
|
||||
BackdropProps={{
|
||||
classes: {
|
||||
root: 'FusePageSimple-backdrop',
|
||||
},
|
||||
}}
|
||||
style={{ position: 'absolute' }}
|
||||
>
|
||||
<FusePageSimpleSidebarContent {...props} />
|
||||
</SwipeableDrawer>
|
||||
</Hidden>
|
||||
|
||||
{variant === 'permanent' && (
|
||||
<Hidden lgDown>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
anchor={position}
|
||||
className={clsx(
|
||||
'FusePageSimple-sidebarWrapper',
|
||||
variant,
|
||||
isOpen ? 'opened' : 'closed',
|
||||
position === 'left' ? 'FusePageSimple-leftSidebar' : 'FusePageSimple-rightSidebar'
|
||||
)}
|
||||
open={isOpen}
|
||||
onClose={props?.onClose}
|
||||
classes={{
|
||||
paper: clsx('FusePageSimple-sidebar border-0', variant),
|
||||
}}
|
||||
>
|
||||
<FusePageSimpleSidebarContent {...props} />
|
||||
</Drawer>
|
||||
</Hidden>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FusePageSimpleSidebar.defaultProps = {
|
||||
open: true,
|
||||
};
|
||||
|
||||
export default FusePageSimpleSidebar;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import FuseScrollbars from '@fuse/core/FuseScrollbars';
|
||||
import { ThemeProvider, useTheme } from '@mui/material/styles';
|
||||
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FusePageSimpleSidebarContent(props) {
|
||||
const theme = useTheme();
|
||||
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
|
||||
|
||||
return (
|
||||
<FuseScrollbars enable={props.innerScroll}>
|
||||
{props.header && (
|
||||
<ThemeProvider theme={contrastTheme}>
|
||||
<div className={clsx('FusePageSimple-sidebarHeader', props.variant)}>{props.header}</div>
|
||||
</ThemeProvider>
|
||||
)}
|
||||
|
||||
{props.content && <div className="FusePageSimple-sidebarContent">{props.content}</div>}
|
||||
</FuseScrollbars>
|
||||
);
|
||||
}
|
||||
|
||||
export default FusePageSimpleSidebarContent;
|
||||
1
src/@fuse/core/FusePageSimple/index.js
Normal file
1
src/@fuse/core/FusePageSimple/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FusePageSimple';
|
||||
197
src/@fuse/core/FuseScrollbars/FuseScrollbars.js
Normal file
197
src/@fuse/core/FuseScrollbars/FuseScrollbars.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MobileDetect from 'mobile-detect';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createRef, forwardRef, useCallback, useEffect, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import history from '@history';
|
||||
import withRouterAndRef from '../withRouterAndRef/withRouterAndRef';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
overscrollBehavior: 'contain',
|
||||
minHeight: '100%',
|
||||
}));
|
||||
|
||||
const md = new MobileDetect(window.navigator.userAgent);
|
||||
const isMobile = md.mobile();
|
||||
|
||||
const handlerNameByEvent = {
|
||||
'ps-scroll-y': 'onScrollY',
|
||||
'ps-scroll-x': 'onScrollX',
|
||||
'ps-scroll-up': 'onScrollUp',
|
||||
'ps-scroll-down': 'onScrollDown',
|
||||
'ps-scroll-left': 'onScrollLeft',
|
||||
'ps-scroll-right': 'onScrollRight',
|
||||
'ps-y-reach-start': 'onYReachStart',
|
||||
'ps-y-reach-end': 'onYReachEnd',
|
||||
'ps-x-reach-start': 'onXReachStart',
|
||||
'ps-x-reach-end': 'onXReachEnd',
|
||||
};
|
||||
Object.freeze(handlerNameByEvent);
|
||||
|
||||
const FuseScrollbars = forwardRef((props, ref) => {
|
||||
ref = ref || createRef();
|
||||
const ps = useRef(null);
|
||||
const handlerByEvent = useRef(new Map());
|
||||
|
||||
const { customScrollbars } = props;
|
||||
|
||||
const hookUpEvents = useCallback(() => {
|
||||
Object.keys(handlerNameByEvent).forEach((key) => {
|
||||
const callback = props[handlerNameByEvent[key]];
|
||||
if (callback) {
|
||||
const handler = () => callback(ref.current);
|
||||
handlerByEvent.current.set(key, handler);
|
||||
ref.current.addEventListener(key, handler, false);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
}, [ref]);
|
||||
|
||||
const unHookUpEvents = useCallback(() => {
|
||||
handlerByEvent.current.forEach((value, key) => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener(key, value, false);
|
||||
}
|
||||
});
|
||||
handlerByEvent.current.clear();
|
||||
}, [ref]);
|
||||
|
||||
const destroyPs = useCallback(() => {
|
||||
// console.info("destroy::ps");
|
||||
|
||||
unHookUpEvents();
|
||||
|
||||
if (!ps.current) {
|
||||
return;
|
||||
}
|
||||
ps.current.destroy();
|
||||
ps.current = null;
|
||||
}, [unHookUpEvents]);
|
||||
|
||||
const createPs = useCallback(() => {
|
||||
// console.info("create::ps");
|
||||
|
||||
if (isMobile || !ref || ps.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ps.current = new PerfectScrollbar(ref.current, props.option);
|
||||
|
||||
hookUpEvents();
|
||||
}, [hookUpEvents, props.option, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
function updatePs() {
|
||||
if (!ps.current) {
|
||||
return;
|
||||
}
|
||||
ps.current.update();
|
||||
}
|
||||
|
||||
updatePs();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (customScrollbars) {
|
||||
createPs();
|
||||
} else {
|
||||
destroyPs();
|
||||
}
|
||||
}, [createPs, customScrollbars, destroyPs]);
|
||||
|
||||
const scrollToTop = useCallback(() => {
|
||||
if (ref && ref.current) {
|
||||
ref.current.scrollTop = 0;
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.scrollToTopOnChildChange) {
|
||||
scrollToTop();
|
||||
}
|
||||
}, [scrollToTop, props.children, props.scrollToTopOnChildChange]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
history.listen(() => {
|
||||
if (props.scrollToTopOnRouteChange) {
|
||||
scrollToTop();
|
||||
}
|
||||
}),
|
||||
[scrollToTop, props.scrollToTopOnRouteChange]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
destroyPs();
|
||||
},
|
||||
[destroyPs]
|
||||
);
|
||||
|
||||
// console.info('render::ps');
|
||||
return (
|
||||
<Root
|
||||
id={props.id}
|
||||
className={props.className}
|
||||
style={
|
||||
props.customScrollbars && (props.enable || true) && !isMobile
|
||||
? {
|
||||
position: 'relative',
|
||||
overflow: 'hidden!important',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
|
||||
function mapStateToProps({ fuse }) {
|
||||
return {
|
||||
customScrollbars: fuse.settings.current.customScrollbars,
|
||||
};
|
||||
}
|
||||
|
||||
FuseScrollbars.propTypes = {
|
||||
onScrollY: PropTypes.func,
|
||||
onScrollX: PropTypes.func,
|
||||
onScrollUp: PropTypes.func,
|
||||
onScrollDown: PropTypes.func,
|
||||
onScrollLeft: PropTypes.func,
|
||||
onScrollRight: PropTypes.func,
|
||||
onYReachStart: PropTypes.func,
|
||||
onYReachEnd: PropTypes.func,
|
||||
onXReachStart: PropTypes.func,
|
||||
onXReachEnd: PropTypes.func,
|
||||
scrollToTopOnRouteChange: PropTypes.bool,
|
||||
scrollToTopOnChildChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
FuseScrollbars.defaultProps = {
|
||||
className: '',
|
||||
enable: true,
|
||||
scrollToTopOnChildChange: false,
|
||||
scrollToTopOnRouteChange: false,
|
||||
option: {
|
||||
wheelPropagation: true,
|
||||
},
|
||||
ref: undefined,
|
||||
onScrollY: undefined,
|
||||
onScrollX: undefined,
|
||||
onScrollUp: undefined,
|
||||
onScrollDown: undefined,
|
||||
onScrollLeft: undefined,
|
||||
onScrollRight: undefined,
|
||||
onYReachStart: undefined,
|
||||
onYReachEnd: undefined,
|
||||
onXReachStart: undefined,
|
||||
onXReachEnd: undefined,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, null, null, { forwardRef: true })(
|
||||
withRouterAndRef(FuseScrollbars)
|
||||
);
|
||||
1
src/@fuse/core/FuseScrollbars/index.js
Normal file
1
src/@fuse/core/FuseScrollbars/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseScrollbars';
|
||||
440
src/@fuse/core/FuseSearch/FuseSearch.js
Normal file
440
src/@fuse/core/FuseSearch/FuseSearch.js
Normal file
@@ -0,0 +1,440 @@
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import match from 'autosuggest-highlight/match';
|
||||
import parse from 'autosuggest-highlight/parse';
|
||||
import clsx from 'clsx';
|
||||
import _ from '@lodash';
|
||||
import { memo, useEffect, useReducer, useRef } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
import FuseSvgIcon from '../FuseSvgIcon';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
'& .FuseSearch-container': {
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
'& .FuseSearch-suggestionsContainerOpen': {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
marginTop: theme.spacing(),
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
|
||||
'& .FuseSearch-suggestion': {
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
'& .FuseSearch-suggestionsList': {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyleType: 'none',
|
||||
},
|
||||
|
||||
'& .FuseSearch-input': {
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
'&:focus': {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function renderInputComponent(inputProps) {
|
||||
const { variant, inputRef = () => {}, ref, ...other } = inputProps;
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
{variant === 'basic' ? (
|
||||
// Outlined
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputRef: (node) => {
|
||||
ref(node);
|
||||
inputRef(node);
|
||||
},
|
||||
classes: {
|
||||
input: 'FuseSearch-input py-0 px-16 h-40 md:h-48 ltr:pr-48 rtl:pl-48',
|
||||
notchedOutline: 'rounded-8',
|
||||
},
|
||||
}}
|
||||
variant="outlined"
|
||||
{...other}
|
||||
/>
|
||||
<FuseSvgIcon
|
||||
className="absolute top-0 ltr:right-0 rtl:left-0 h-40 md:h-48 w-48 p-12 pointer-events-none"
|
||||
color="action"
|
||||
>
|
||||
heroicons-outline:search
|
||||
</FuseSvgIcon>
|
||||
</>
|
||||
) : (
|
||||
// Standard
|
||||
<TextField
|
||||
fullWidth
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
inputRef: (node) => {
|
||||
ref(node);
|
||||
inputRef(node);
|
||||
},
|
||||
classes: {
|
||||
input: 'FuseSearch-input py-0 px-16 h-48 md:h-64',
|
||||
},
|
||||
}}
|
||||
variant="standard"
|
||||
{...other}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSuggestion(suggestion, { query, isHighlighted }) {
|
||||
const matches = match(suggestion.title, query);
|
||||
const parts = parse(suggestion.title, matches);
|
||||
|
||||
return (
|
||||
<MenuItem selected={isHighlighted} component="div">
|
||||
<ListItemIcon className="min-w-40">
|
||||
{suggestion.icon ? (
|
||||
<FuseSvgIcon>{suggestion.icon}</FuseSvgIcon>
|
||||
) : (
|
||||
<span className="text-20 w-24 font-semibold uppercase text-center">
|
||||
{suggestion.title[0]}
|
||||
</span>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={parts.map((part, index) =>
|
||||
part.highlight ? (
|
||||
<span key={String(index)} style={{ fontWeight: 600 }}>
|
||||
{part.text}
|
||||
</span>
|
||||
) : (
|
||||
<strong key={String(index)} style={{ fontWeight: 300 }}>
|
||||
{part.text}
|
||||
</strong>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function getSuggestions(value, data) {
|
||||
const inputValue = _.deburr(value.trim()).toLowerCase();
|
||||
const inputLength = inputValue.length;
|
||||
let count = 0;
|
||||
|
||||
return inputLength === 0
|
||||
? []
|
||||
: data.filter((suggestion) => {
|
||||
const keep = count < 10 && match(suggestion.title, inputValue).length > 0;
|
||||
|
||||
if (keep) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return keep;
|
||||
});
|
||||
}
|
||||
|
||||
function getSuggestionValue(suggestion) {
|
||||
return suggestion.title;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
searchText: '',
|
||||
search: false,
|
||||
navigation: null,
|
||||
suggestions: [],
|
||||
noSuggestions: false,
|
||||
};
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'open': {
|
||||
return {
|
||||
...state,
|
||||
opened: true,
|
||||
};
|
||||
}
|
||||
case 'close': {
|
||||
return {
|
||||
...state,
|
||||
opened: false,
|
||||
searchText: '',
|
||||
};
|
||||
}
|
||||
case 'setSearchText': {
|
||||
return {
|
||||
...state,
|
||||
searchText: action.value,
|
||||
};
|
||||
}
|
||||
case 'setNavigation': {
|
||||
return {
|
||||
...state,
|
||||
navigation: action.value,
|
||||
};
|
||||
}
|
||||
case 'updateSuggestions': {
|
||||
const suggestions = getSuggestions(action.value, state.navigation);
|
||||
const isInputBlank = action.value.trim() === '';
|
||||
const noSuggestions = !isInputBlank && suggestions.length === 0;
|
||||
|
||||
return {
|
||||
...state,
|
||||
suggestions,
|
||||
noSuggestions,
|
||||
};
|
||||
}
|
||||
case 'clearSuggestions': {
|
||||
return {
|
||||
...state,
|
||||
suggestions: [],
|
||||
noSuggestions: false,
|
||||
};
|
||||
}
|
||||
case 'decrement': {
|
||||
return { count: state.count - 1 };
|
||||
}
|
||||
default: {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function FuseSearch(props) {
|
||||
const { navigation } = props;
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const suggestionsNode = useRef(null);
|
||||
const popperNode = useRef(null);
|
||||
const buttonNode = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'setNavigation',
|
||||
value: navigation,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
function showSearch(ev) {
|
||||
ev.stopPropagation();
|
||||
dispatch({ type: 'open' });
|
||||
document.addEventListener('keydown', escFunction, false);
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
dispatch({ type: 'close' });
|
||||
document.removeEventListener('keydown', escFunction, false);
|
||||
}
|
||||
|
||||
function escFunction(event) {
|
||||
if (event.keyCode === 27) {
|
||||
hideSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuggestionsFetchRequested({ value }) {
|
||||
dispatch({
|
||||
type: 'updateSuggestions',
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSuggestionSelected(event, { suggestion }) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!suggestion.url) {
|
||||
return;
|
||||
}
|
||||
props.navigate(suggestion.url);
|
||||
hideSearch();
|
||||
}
|
||||
|
||||
function handleSuggestionsClearRequested() {
|
||||
dispatch({
|
||||
type: 'clearSuggestions',
|
||||
});
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
dispatch({
|
||||
type: 'setSearchText',
|
||||
value: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
function handleClickAway(event) {
|
||||
return (
|
||||
state.opened &&
|
||||
(!suggestionsNode.current || !suggestionsNode.current.contains(event.target)) &&
|
||||
hideSearch()
|
||||
);
|
||||
}
|
||||
|
||||
const autosuggestProps = {
|
||||
renderInputComponent,
|
||||
highlightFirstSuggestion: true,
|
||||
suggestions: state.suggestions,
|
||||
onSuggestionsFetchRequested: handleSuggestionsFetchRequested,
|
||||
onSuggestionsClearRequested: handleSuggestionsClearRequested,
|
||||
onSuggestionSelected: handleSuggestionSelected,
|
||||
getSuggestionValue,
|
||||
renderSuggestion,
|
||||
};
|
||||
|
||||
switch (props.variant) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<div className={clsx('flex items-center w-full', props.className)} ref={popperNode}>
|
||||
<Autosuggest
|
||||
{...autosuggestProps}
|
||||
inputProps={{
|
||||
variant: props.variant,
|
||||
placeholder: props.placeholder,
|
||||
value: state.searchText,
|
||||
onChange: handleChange,
|
||||
onFocus: showSearch,
|
||||
InputLabelProps: {
|
||||
shrink: true,
|
||||
},
|
||||
autoFocus: false,
|
||||
}}
|
||||
theme={{
|
||||
container: 'flex flex-1 w-full',
|
||||
suggestionsList: 'FuseSearch-suggestionsList',
|
||||
suggestion: 'FuseSearch-suggestion',
|
||||
}}
|
||||
renderSuggestionsContainer={(options) => (
|
||||
<Popper
|
||||
anchorEl={popperNode.current}
|
||||
open={Boolean(options.children) || state.noSuggestions}
|
||||
popperOptions={{ positionFixed: true }}
|
||||
className="z-9999"
|
||||
>
|
||||
<div ref={suggestionsNode}>
|
||||
<Paper
|
||||
className="shadow-lg rounded-8 overflow-hidden"
|
||||
{...options.containerProps}
|
||||
style={{ width: popperNode.current ? popperNode.current.clientWidth : null }}
|
||||
>
|
||||
{options.children}
|
||||
{state.noSuggestions && (
|
||||
<Typography className="px-16 py-12">{props.noResults}</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
</Popper>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'full': {
|
||||
return (
|
||||
<Root className={clsx('flex', props.className)}>
|
||||
<Tooltip title="Click to search" placement="bottom">
|
||||
<div
|
||||
onClick={showSearch}
|
||||
onKeyDown={showSearch}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
ref={buttonNode}
|
||||
>
|
||||
{props.trigger}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{state.opened && (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<Paper className="absolute left-0 right-0 top-0 h-full z-9999 shadow-0" square>
|
||||
<div className="flex items-center w-full h-full" ref={popperNode}>
|
||||
<Autosuggest
|
||||
{...autosuggestProps}
|
||||
inputProps={{
|
||||
placeholder: props.placeholder,
|
||||
value: state.searchText,
|
||||
onChange: handleChange,
|
||||
InputLabelProps: {
|
||||
shrink: true,
|
||||
},
|
||||
autoFocus: true,
|
||||
}}
|
||||
theme={{
|
||||
container: 'flex flex-1 w-full',
|
||||
suggestionsList: 'FuseSearch-suggestionsList',
|
||||
suggestion: 'FuseSearch-suggestion',
|
||||
}}
|
||||
renderSuggestionsContainer={(options) => (
|
||||
<Popper
|
||||
anchorEl={popperNode.current}
|
||||
open={Boolean(options.children) || state.noSuggestions}
|
||||
popperOptions={{ positionFixed: true }}
|
||||
className="z-9999"
|
||||
>
|
||||
<div ref={suggestionsNode}>
|
||||
<Paper
|
||||
className="shadow-lg"
|
||||
square
|
||||
{...options.containerProps}
|
||||
style={{
|
||||
width: popperNode.current ? popperNode.current.clientWidth : null,
|
||||
}}
|
||||
>
|
||||
{options.children}
|
||||
{state.noSuggestions && (
|
||||
<Typography className="px-16 py-12">{props.noResults}</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
</Popper>
|
||||
)}
|
||||
/>
|
||||
<IconButton onClick={hideSearch} className="mx-8" size="large">
|
||||
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FuseSearch.propTypes = {};
|
||||
FuseSearch.defaultProps = {
|
||||
navigation: [],
|
||||
trigger: (
|
||||
<IconButton className="w-40 h-40" size="large">
|
||||
<FuseSvgIcon>heroicons-outline:search</FuseSvgIcon>
|
||||
</IconButton>
|
||||
),
|
||||
variant: 'full',
|
||||
placeholder: 'Search',
|
||||
noResults: 'No results..',
|
||||
};
|
||||
|
||||
export default withRouter(memo(FuseSearch));
|
||||
1
src/@fuse/core/FuseSearch/index.js
Normal file
1
src/@fuse/core/FuseSearch/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSearch';
|
||||
464
src/@fuse/core/FuseSettings/FuseSettings.js
Normal file
464
src/@fuse/core/FuseSettings/FuseSettings.js
Normal file
@@ -0,0 +1,464 @@
|
||||
import { useDebounce, usePrevious } from '@fuse/hooks';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import themeLayoutConfigs from 'app/theme-layouts/themeLayoutConfigs';
|
||||
import _ from '@lodash';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import Select from '@mui/material/Select';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
selectFuseCurrentSettings,
|
||||
selectFuseThemesSettings,
|
||||
setDefaultSettings,
|
||||
} from 'app/store/fuse/settingsSlice';
|
||||
import { selectUser } from 'app/store/userSlice';
|
||||
import PaletteSelector from './palette-generator/PaletteSelector';
|
||||
import SectionPreview from './palette-generator/SectionPreview';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
'& .FuseSettings-formControl': {
|
||||
margin: '6px 0',
|
||||
width: '100%',
|
||||
'&:last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'& .FuseSettings-group': {},
|
||||
|
||||
'& .FuseSettings-formGroupTitle': {
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
left: 8,
|
||||
fontWeight: 600,
|
||||
padding: '0 4px',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
'& .FuseSettings-formGroup': {
|
||||
position: 'relative',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2,
|
||||
padding: '12px 12px 0 12px',
|
||||
margin: '24px 0 16px 0',
|
||||
'&:first-of-type': {
|
||||
marginTop: 16,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseSettings(props) {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector(selectUser);
|
||||
const themes = useSelector(selectFuseThemesSettings);
|
||||
const settings = useSelector(selectFuseCurrentSettings);
|
||||
const { reset, watch, control } = useForm({
|
||||
mode: 'onChange',
|
||||
defaultValues: settings,
|
||||
});
|
||||
const form = watch();
|
||||
const { form: formConfigs } = themeLayoutConfigs[form.layout.style];
|
||||
const prevForm = usePrevious(form ? _.merge({}, form) : null);
|
||||
const prevSettings = usePrevious(settings ? _.merge({}, settings) : null);
|
||||
const formChanged = !_.isEqual(form, prevForm);
|
||||
const settingsChanged = !_.isEqual(settings, prevSettings);
|
||||
|
||||
const handleUpdate = useDebounce((newSettings) => {
|
||||
dispatch(setDefaultSettings(newSettings));
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip inital changes
|
||||
if (!prevForm && !prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme settings changed update form data
|
||||
if (settingsChanged) {
|
||||
reset(settings);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSettings = _.merge({}, settings, form);
|
||||
|
||||
// No need to change
|
||||
if (_.isEqual(newSettings, settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If form changed update theme settings
|
||||
if (formChanged) {
|
||||
if (settings.layout.style !== newSettings.layout.style) {
|
||||
_.set(
|
||||
newSettings,
|
||||
'layout.config',
|
||||
themeLayoutConfigs[newSettings?.layout?.style]?.defaults
|
||||
);
|
||||
}
|
||||
handleUpdate(newSettings);
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
form,
|
||||
formChanged,
|
||||
handleUpdate,
|
||||
prevForm,
|
||||
prevSettings,
|
||||
reset,
|
||||
settings,
|
||||
settingsChanged,
|
||||
user,
|
||||
]);
|
||||
|
||||
const ThemeSelect = ({ value, name, handleThemeChange }) => {
|
||||
return (
|
||||
<Select
|
||||
className="w-full rounded-8 h-40 overflow-hidden my-8"
|
||||
value={value}
|
||||
onChange={handleThemeChange}
|
||||
name={name}
|
||||
variant="outlined"
|
||||
style={{
|
||||
backgroundColor: themes[value].palette.background.default,
|
||||
color: themes[value].palette.mode === 'light' ? '#000000' : '#ffffff',
|
||||
}}
|
||||
>
|
||||
{Object.entries(themes)
|
||||
.filter(
|
||||
([key, val]) =>
|
||||
!(name === 'theme.main' && (key === 'mainThemeDark' || key === 'mainThemeLight'))
|
||||
)
|
||||
.map(([key, val]) => (
|
||||
<MenuItem
|
||||
key={key}
|
||||
value={key}
|
||||
className="m-8 mt-0 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: val.palette.background.default,
|
||||
color: val.palette.mode === 'light' ? '#000000' : '#FFFFFF',
|
||||
border: `1px solid ${
|
||||
val.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
{_.startCase(key)}
|
||||
<div
|
||||
className="flex w-full h-8 block absolute bottom-0 left-0 right-0"
|
||||
style={{
|
||||
borderTop: `1px solid ${
|
||||
val.palette.mode === 'light'
|
||||
? 'rgba(0, 0, 0, 0.12)'
|
||||
: 'rgba(255, 255, 255, 0.12)'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-1/4 h-8"
|
||||
style={{
|
||||
backgroundColor: val.palette.primary.main
|
||||
? val.palette.primary.main
|
||||
: val.palette.primary[500],
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="w-1/4 h-8"
|
||||
style={{
|
||||
backgroundColor: val.palette.secondary.main
|
||||
? val.palette.secondary.main
|
||||
: val.palette.secondary[500],
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="w-1/4 h-8"
|
||||
style={{
|
||||
backgroundColor: val.palette.error.main
|
||||
? val.palette.error.main
|
||||
: val.palette.error[500],
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="w-1/4 h-8"
|
||||
style={{ backgroundColor: val.palette.background.paper }}
|
||||
/>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const getForm = useCallback(
|
||||
(_formConfigs, prefix) =>
|
||||
Object.entries(_formConfigs).map(([key, formControl]) => {
|
||||
const target = prefix ? `${prefix}.${key}` : key;
|
||||
switch (formControl.type) {
|
||||
case 'radio': {
|
||||
return (
|
||||
<Controller
|
||||
key={target}
|
||||
name={target}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl component="fieldset" className="FuseSettings-formControl">
|
||||
<FormLabel component="legend" className="text-14">
|
||||
{formControl.title}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
aria-label={formControl.title}
|
||||
className="FuseSettings-group"
|
||||
row={formControl.options.length < 4}
|
||||
>
|
||||
{formControl.options.map((opt) => (
|
||||
<FormControlLabel
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
control={<Radio />}
|
||||
label={opt.name}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'switch': {
|
||||
return (
|
||||
<Controller
|
||||
key={target}
|
||||
name={target}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl component="fieldset" className="FuseSettings-formControl">
|
||||
<FormLabel component="legend" className="text-14">
|
||||
{formControl.title}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(ev) => onChange(ev.target.checked)}
|
||||
aria-label={formControl.title}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'number': {
|
||||
return (
|
||||
<div key={target} className="FuseSettings-formControl">
|
||||
<Controller
|
||||
name={target}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label={formControl.title}
|
||||
type="number"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'group': {
|
||||
return (
|
||||
<div key={target} className="FuseSettings-formGroup">
|
||||
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
|
||||
{formControl.title}
|
||||
</Typography>
|
||||
|
||||
{getForm(formControl.children, target)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}),
|
||||
[control]
|
||||
);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<div className="FuseSettings-formGroup">
|
||||
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
|
||||
Layout
|
||||
</Typography>
|
||||
|
||||
<Controller
|
||||
name="layout.style"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl component="fieldset" className="FuseSettings-formControl">
|
||||
<FormLabel component="legend" className="text-14">
|
||||
Style
|
||||
</FormLabel>
|
||||
<RadioGroup {...field} aria-label="Layout Style" className="FuseSettings-group">
|
||||
{Object.entries(themeLayoutConfigs).map(([key, layout]) => (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
value={key}
|
||||
control={<Radio />}
|
||||
label={layout.title}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{useMemo(() => getForm(formConfigs, 'layout.config'), [formConfigs, getForm])}
|
||||
|
||||
<Typography className="my-16 text-12 italic" color="text.secondary">
|
||||
*Not all option combinations are available
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="FuseSettings-formGroup pb-16">
|
||||
<Typography className="FuseSettings-formGroupTitle" color="text.secondary">
|
||||
Theme
|
||||
</Typography>
|
||||
|
||||
<div className="flex flex-wrap -mx-8">
|
||||
<Controller
|
||||
name="theme.main"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<PaletteSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
trigger={
|
||||
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
|
||||
<SectionPreview
|
||||
className="group-hover:shadow-lg transition-shadow"
|
||||
section="main"
|
||||
/>
|
||||
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
|
||||
Main Palette
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="theme.navbar"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<PaletteSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
trigger={
|
||||
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
|
||||
<SectionPreview
|
||||
className="group-hover:shadow-lg transition-shadow"
|
||||
section="navbar"
|
||||
/>
|
||||
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
|
||||
Navbar Palette
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="theme.toolbar"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<PaletteSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
trigger={
|
||||
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
|
||||
<SectionPreview
|
||||
className="group-hover:shadow-lg transition-shadow"
|
||||
section="toolbar"
|
||||
/>
|
||||
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
|
||||
Toolbar Palette
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="theme.footer"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<PaletteSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
trigger={
|
||||
<div className="flex flex-col items-center space-y-8 w-128 m-8 cursor-pointer group">
|
||||
<SectionPreview
|
||||
className="group-hover:shadow-lg transition-shadow"
|
||||
section="footer"
|
||||
/>
|
||||
<Typography className="flex-1 text-14 font-semibold mb-24 opacity-80 group-hover:opacity-100">
|
||||
Footer Palette
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="customScrollbars"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl component="fieldset" className="FuseSettings-formControl">
|
||||
<FormLabel component="legend" className="text-14">
|
||||
Custom Scrollbars
|
||||
</FormLabel>
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(ev) => onChange(ev.target.checked)}
|
||||
aria-label="Custom Scrollbars"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="direction"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl component="fieldset" className="FuseSettings-formControl">
|
||||
<FormLabel component="legend" className="text-14">
|
||||
Direction
|
||||
</FormLabel>
|
||||
<RadioGroup {...field} aria-label="Layout Direction" className="FuseSettings-group" row>
|
||||
<FormControlLabel key="rtl" value="rtl" control={<Radio />} label="RTL" />
|
||||
<FormControlLabel key="ltr" value="ltr" control={<Radio />} label="LTR" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FuseSettings);
|
||||
1
src/@fuse/core/FuseSettings/index.js
Normal file
1
src/@fuse/core/FuseSettings/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSettings';
|
||||
@@ -0,0 +1,62 @@
|
||||
import clsx from 'clsx';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
function PalettePreview(props) {
|
||||
const { palette, className } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={clsx(
|
||||
'w-200 text-left rounded-6 relative font-bold shadow overflow-hidden',
|
||||
className
|
||||
)}
|
||||
sx={{
|
||||
backgroundColor: palette.background.default,
|
||||
color: palette.text.primary,
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Box
|
||||
className="w-full h-56 px-8 pt-8 relative"
|
||||
sx={{
|
||||
backgroundColor: palette.primary.main,
|
||||
color: (theme) =>
|
||||
palette.primary.contrastText || theme.palette.getContrastText(palette.primary.main),
|
||||
}}
|
||||
>
|
||||
<span className="text-12">Header (Primary)</span>
|
||||
|
||||
<Box
|
||||
className="flex items-center justify-center w-20 h-20 rounded-full absolute bottom-0 right-0 -mb-10 shadow text-10 mr-4"
|
||||
sx={{
|
||||
backgroundColor: palette.secondary.main,
|
||||
color: (theme) =>
|
||||
palette.secondary.contrastText ||
|
||||
theme.palette.getContrastText(palette.secondary.main),
|
||||
}}
|
||||
>
|
||||
<span className="">S</span>
|
||||
</Box>
|
||||
</Box>
|
||||
<div className="pl-8 pr-28 -mt-24 w-full">
|
||||
<Box
|
||||
className="w-full h-96 rounded-4 relative shadow p-8"
|
||||
sx={{
|
||||
backgroundColor: palette.background.paper,
|
||||
color: palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<span className="text-12 opacity-75">Paper</span>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<div className="px-8 py-8 w-full">
|
||||
<span className="text-12 opacity-75">Background</span>
|
||||
</div>
|
||||
|
||||
{/* <pre className="language-js p-24 w-400">{JSON.stringify(palette, null, 2)}</pre> */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PalettePreview;
|
||||
282
src/@fuse/core/FuseSettings/palette-generator/PaletteSelector.js
Normal file
282
src/@fuse/core/FuseSettings/palette-generator/PaletteSelector.js
Normal file
@@ -0,0 +1,282 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import _ from '@lodash';
|
||||
import { darkPaletteText, lightPaletteText } from 'app/configs/themesConfig';
|
||||
import { darken, getContrastRatio, lighten } from '@mui/material/styles';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { Dialog, DialogActions, DialogContent, Icon, TextField } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Button from '@mui/material/Button';
|
||||
import SectionPreview from './SectionPreview';
|
||||
import PalettePreview from './PalettePreview';
|
||||
|
||||
function isDark(color) {
|
||||
return getContrastRatio(color, '#ffffff') >= 3;
|
||||
}
|
||||
|
||||
function PaletteSelector(props) {
|
||||
const { value } = props;
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const { reset, formState, trigger, handleSubmit, watch, control, setValue } = methods;
|
||||
|
||||
const { isValid, dirtyFields, errors } = formState;
|
||||
|
||||
useEffect(() => {
|
||||
reset(value);
|
||||
}, [value, reset]);
|
||||
|
||||
const form = watch();
|
||||
const formType = watch('palette.mode');
|
||||
|
||||
useEffect(() => {
|
||||
// console.info(form);
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formType || !openDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => trigger(['palette.background.paper', 'palette.background.default']));
|
||||
}, [formType, openDialog, trigger]);
|
||||
|
||||
const backgroundColorValidation = (v) => {
|
||||
if (formType === 'light' && isDark(v)) {
|
||||
return 'Must be a light color';
|
||||
}
|
||||
if (formType === 'dark' && !isDark(v)) {
|
||||
return 'Must be a dark color';
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open Dialog
|
||||
*/
|
||||
function handleOpenDialog(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setOpenDialog(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Dialog
|
||||
*/
|
||||
function handleCloseDialog() {
|
||||
setOpenDialog(false);
|
||||
}
|
||||
|
||||
function onSubmit(formData) {
|
||||
props.onChange(formData);
|
||||
handleCloseDialog();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus */}
|
||||
<div onClick={handleOpenDialog} role="button">
|
||||
{props.trigger}
|
||||
</div>
|
||||
<Dialog
|
||||
container={document.body}
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
aria-labelledby="form-dialog-title"
|
||||
classes={{
|
||||
paper: 'rounded-5 w-full',
|
||||
}}
|
||||
>
|
||||
<AppBar elevation={0} position="static">
|
||||
<Toolbar className="flex w-full">
|
||||
<Icon className="mr-12">palette</Icon>
|
||||
<Typography variant="subtitle1" color="inherit">
|
||||
Edit palette
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<DialogContent>
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-col items-center justify-center p-24 flex-1">
|
||||
<Controller
|
||||
name="palette.mode"
|
||||
control={control}
|
||||
render={({ field: { onChange: _onChange, value: _value } }) => (
|
||||
<ButtonGroup
|
||||
disableElevation
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className="mb-32"
|
||||
>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
_onChange('light');
|
||||
setValue('palette.text', lightPaletteText, { shouldDirty: true });
|
||||
}}
|
||||
variant={_value === 'light' ? 'contained' : 'outlined'}
|
||||
>
|
||||
Light
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
_onChange('dark');
|
||||
setValue('palette.text', darkPaletteText, { shouldDirty: true });
|
||||
}}
|
||||
variant={_value === 'dark' ? 'contained' : 'outlined'}
|
||||
>
|
||||
Dark
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="palette.primary.main"
|
||||
control={control}
|
||||
render={({ field: { onChange: _onChange, value: _value } }) => (
|
||||
<TextField
|
||||
value={_value}
|
||||
onChange={(ev) => {
|
||||
_onChange(ev.target.value);
|
||||
setValue('palette.primary.light', lighten(ev.target.value, 0.8), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue('palette.primary.dark', darken(ev.target.value, 0.2), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue(
|
||||
'palette.primary.contrastText',
|
||||
theme.palette.getContrastText(ev.target.value),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
type="color"
|
||||
variant="outlined"
|
||||
className="mb-32"
|
||||
label="Primary color"
|
||||
InputProps={{ className: 'w-200 h-32' }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="palette.secondary.main"
|
||||
control={control}
|
||||
render={({ field: { onChange: _onChange, value: _value } }) => (
|
||||
<TextField
|
||||
value={_value}
|
||||
onChange={(ev) => {
|
||||
_onChange(ev.target.value);
|
||||
setValue('palette.secondary.light', lighten(ev.target.value, 0.8), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue('palette.secondary.dark', darken(ev.target.value, 0.2), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue(
|
||||
'palette.secondary.contrastText',
|
||||
theme.palette.getContrastText(ev.target.value),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
type="color"
|
||||
variant="outlined"
|
||||
className="mb-32"
|
||||
label="Secondary color"
|
||||
InputProps={{ className: 'w-200 h-32' }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="palette.background.paper"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
backgroundColorValidation,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="color"
|
||||
variant="outlined"
|
||||
className="mb-32"
|
||||
label="Background paper"
|
||||
InputProps={{ className: 'w-200 h-32' }}
|
||||
error={!!errors?.palette?.background?.paper}
|
||||
helperText={errors?.palette?.background?.paper?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="palette.background.default"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
backgroundColorValidation,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="color"
|
||||
variant="outlined"
|
||||
className=""
|
||||
label="Background default"
|
||||
InputProps={{ className: 'w-200 h-32' }}
|
||||
error={!!errors?.palette?.background?.default}
|
||||
helperText={errors?.palette?.background?.default?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-48">
|
||||
<Typography className="text-16 font-semibold mb-16 -mt-48" color="text.secondary">
|
||||
Preview
|
||||
</Typography>
|
||||
<PalettePreview className="" palette={form.palette} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions className="flex justify-between p-16">
|
||||
<Button onClick={handleCloseDialog} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
autoFocus
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PaletteSelector.defaultProps = {
|
||||
trigger: (
|
||||
<div className="flex flex-col items-center space-y-8 w-128 m-8">
|
||||
<SectionPreview section="" />
|
||||
<Typography className="flex-1 text-16 font-bold mb-24">Edit Palette</Typography>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
export default PaletteSelector;
|
||||
110
src/@fuse/core/FuseSettings/palette-generator/SectionPreview.js
Normal file
110
src/@fuse/core/FuseSettings/palette-generator/SectionPreview.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import clsx from 'clsx';
|
||||
import Box from '@mui/material/Box';
|
||||
import { darken, lighten } from '@mui/material/styles';
|
||||
import { red } from '@mui/material/colors';
|
||||
|
||||
function SectionPreview(props) {
|
||||
const { section, className } = props;
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-128 h-80 rounded-md overflow-hidden border-1 hover:opacity-80',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
section === 'navbar'
|
||||
? red['100']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? lighten(theme.palette.background.default, 0.4)
|
||||
: lighten(theme.palette.background.default, 0.02),
|
||||
'& > div': {
|
||||
backgroundColor:
|
||||
section === 'navbar'
|
||||
? red['200']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? darken(theme.palette.background.default, 0.1)
|
||||
: lighten(theme.palette.background.default, 0.1),
|
||||
},
|
||||
}}
|
||||
className="w-32 pt-12 px-6 space-y-1"
|
||||
>
|
||||
<div className="h-4 rounded-sm" />
|
||||
<div className="h-4 rounded-sm" />
|
||||
<div className="h-4 rounded-sm" />
|
||||
<div className="h-4 rounded-sm" />
|
||||
<div className="h-4 rounded-sm" />
|
||||
</Box>
|
||||
<div className="flex flex-col flex-auto border-l">
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
section === 'toolbar'
|
||||
? red['100']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? lighten(theme.palette.background.default, 0.4)
|
||||
: lighten(theme.palette.background.default, 0.02),
|
||||
'& > div': {
|
||||
backgroundColor:
|
||||
section === 'toolbar'
|
||||
? red['200']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? darken(theme.palette.background.default, 0.1)
|
||||
: lighten(theme.palette.background.default, 0.1),
|
||||
},
|
||||
}}
|
||||
className={clsx('h-12 flex items-center justify-end h-full pr-6')}
|
||||
>
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
section === 'main'
|
||||
? red['100']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? lighten(theme.palette.background.default, 0.4)
|
||||
: lighten(theme.palette.background.default, 0.02),
|
||||
}}
|
||||
className={clsx('flex flex-auto border-t border-b')}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
section === 'footer'
|
||||
? red['100']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? lighten(theme.palette.background.default, 0.4)
|
||||
: lighten(theme.palette.background.default, 0.02),
|
||||
'& > div': {
|
||||
backgroundColor:
|
||||
section === 'footer'
|
||||
? red['200']
|
||||
: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? darken(theme.palette.background.default, 0.1)
|
||||
: lighten(theme.palette.background.default, 0.1),
|
||||
},
|
||||
}}
|
||||
className={clsx('h-12 flex items-center pr-6')}
|
||||
>
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
<div className="w-4 h-4 ml-4 rounded-full" />
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SectionPreview;
|
||||
234
src/@fuse/core/FuseShortcuts/FuseShortcuts.js
Normal file
234
src/@fuse/core/FuseShortcuts/FuseShortcuts.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import { amber } from '@mui/material/colors';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Input from '@mui/material/Input';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import FuseSvgIcon from '../FuseSvgIcon';
|
||||
|
||||
function FuseShortcuts(props) {
|
||||
const { navigation, shortcuts, onChange } = props;
|
||||
|
||||
const searchInputRef = useRef(null);
|
||||
const [addMenu, setAddMenu] = useState(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [searchResults, setSearchResults] = useState(null);
|
||||
const shortcutItems = shortcuts
|
||||
? shortcuts.map((id) => navigation.find((item) => item.id === id))
|
||||
: [];
|
||||
|
||||
function addMenuClick(event) {
|
||||
setAddMenu(event.currentTarget);
|
||||
}
|
||||
|
||||
function addMenuClose() {
|
||||
setAddMenu(null);
|
||||
}
|
||||
|
||||
function search(ev) {
|
||||
const newSearchText = ev.target.value;
|
||||
|
||||
setSearchText(newSearchText);
|
||||
|
||||
if (newSearchText.length !== 0 && navigation) {
|
||||
setSearchResults(
|
||||
navigation.filter((item) => item.title.toLowerCase().includes(newSearchText.toLowerCase()))
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSearchResults(null);
|
||||
}
|
||||
|
||||
function toggleInShortcuts(id) {
|
||||
let newShortcuts = [...shortcuts];
|
||||
newShortcuts = newShortcuts.includes(id)
|
||||
? newShortcuts.filter((_id) => id !== _id)
|
||||
: [...newShortcuts, id];
|
||||
onChange(newShortcuts);
|
||||
}
|
||||
|
||||
function ShortcutMenuItem({ item, onToggle }) {
|
||||
if (!item || !item.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={item.url || ''} role="button">
|
||||
<MenuItem key={item.id}>
|
||||
<ListItemIcon className="min-w-40">
|
||||
{item.icon ? (
|
||||
<FuseSvgIcon>{item.icon}</FuseSvgIcon>
|
||||
) : (
|
||||
<span className="text-20 font-semibold uppercase text-center">{item.title[0]}</span>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={item.title} />
|
||||
<IconButton
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onToggle(item.id);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon color="action">
|
||||
{shortcuts.includes(item.id) ? 'heroicons-solid:star' : 'heroicons-outline:star'}
|
||||
</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-1',
|
||||
props.variant === 'vertical' && 'flex-col grow-0 shrink',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{useMemo(() => {
|
||||
const container = {
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const item = {
|
||||
hidden: { opacity: 0, scale: 0.6 },
|
||||
show: { opacity: 1, scale: 1 },
|
||||
};
|
||||
return (
|
||||
<motion.div
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className={clsx('flex flex-1', props.variant === 'vertical' && 'flex-col')}
|
||||
>
|
||||
{shortcutItems.map(
|
||||
(_item) =>
|
||||
_item && (
|
||||
<Link to={_item.url} key={_item.id} role="button">
|
||||
<Tooltip
|
||||
title={_item.title}
|
||||
placement={props.variant === 'horizontal' ? 'bottom' : 'left'}
|
||||
>
|
||||
<IconButton
|
||||
className="w-40 h-40 p-0"
|
||||
component={motion.div}
|
||||
variants={item}
|
||||
size="large"
|
||||
>
|
||||
{_item.icon ? (
|
||||
<FuseSvgIcon>{_item.icon}</FuseSvgIcon>
|
||||
) : (
|
||||
<span className="text-20 font-semibold uppercase">{_item.title[0]}</span>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
title="Click to add/remove shortcut"
|
||||
placement={props.variant === 'horizontal' ? 'bottom' : 'left'}
|
||||
>
|
||||
<IconButton
|
||||
component={motion.div}
|
||||
variants={item}
|
||||
className="w-40 h-40 p-0"
|
||||
aria-owns={addMenu ? 'add-menu' : null}
|
||||
aria-haspopup="true"
|
||||
onClick={addMenuClick}
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon sx={{ color: amber[600] }}>heroicons-solid:star</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
);
|
||||
}, [addMenu, props.variant, shortcutItems])}
|
||||
|
||||
<Menu
|
||||
id="add-menu"
|
||||
anchorEl={addMenu}
|
||||
open={Boolean(addMenu)}
|
||||
onClose={addMenuClose}
|
||||
classes={{
|
||||
paper: 'min-w-256',
|
||||
}}
|
||||
TransitionProps={{
|
||||
onEntered: () => {
|
||||
searchInputRef.current.focus();
|
||||
},
|
||||
onExited: () => {
|
||||
setSearchText('');
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="p-16 pt-8">
|
||||
<Input
|
||||
inputRef={searchInputRef}
|
||||
value={searchText}
|
||||
onChange={search}
|
||||
placeholder="Search for an app or page"
|
||||
className=""
|
||||
fullWidth
|
||||
inputProps={{
|
||||
'aria-label': 'Search',
|
||||
}}
|
||||
disableUnderline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{searchText.length !== 0 &&
|
||||
searchResults &&
|
||||
searchResults.map((_item) => (
|
||||
<ShortcutMenuItem
|
||||
key={_item.id}
|
||||
item={_item}
|
||||
onToggle={() => toggleInShortcuts(_item.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{searchText.length !== 0 && searchResults.length === 0 && (
|
||||
<Typography color="text.secondary" className="p-16 pb-8">
|
||||
No results..
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{searchText.length === 0 &&
|
||||
shortcutItems.map(
|
||||
(_item) =>
|
||||
_item && (
|
||||
<ShortcutMenuItem
|
||||
key={_item.id}
|
||||
item={_item}
|
||||
onToggle={() => toggleInShortcuts(_item.id)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FuseShortcuts.propTypes = {};
|
||||
FuseShortcuts.defaultProps = {
|
||||
variant: 'horizontal',
|
||||
};
|
||||
|
||||
export default memo(FuseShortcuts);
|
||||
1
src/@fuse/core/FuseShortcuts/index.js
Normal file
1
src/@fuse/core/FuseShortcuts/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseShortcuts';
|
||||
261
src/@fuse/core/FuseSidePanel/FuseSidePanel.js
Normal file
261
src/@fuse/core/FuseSidePanel/FuseSidePanel.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import FuseScrollbars from '@fuse/core/FuseScrollbars';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import Hidden from '@mui/material/Hidden';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useState } from 'react';
|
||||
import FuseSvgIcon from '../FuseSvgIcon';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
'& .FuseSidePanel-paper': {
|
||||
display: 'flex',
|
||||
width: 56,
|
||||
transition: theme.transitions.create(['transform', 'width', 'min-width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
paddingBottom: 64,
|
||||
height: '100%',
|
||||
maxHeight: '100vh',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
'&.left': {
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
left: 0,
|
||||
right: 'auto',
|
||||
},
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
},
|
||||
'&.right': {
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
right: 0,
|
||||
left: 'auto',
|
||||
},
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
},
|
||||
'&.closed': {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
width: 0,
|
||||
},
|
||||
'&.left': {
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
justifyContent: 'start',
|
||||
},
|
||||
'& .FuseSidePanel-button': {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
paddingLeft: 4,
|
||||
},
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
},
|
||||
'&.right': {
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
'& .FuseSidePanel-button': {
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
paddingRight: 4,
|
||||
},
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
},
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
width: 'auto',
|
||||
},
|
||||
'& .FuseSidePanel-button': {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: 38,
|
||||
transition: theme.transitions.create(
|
||||
['background-color', 'border-radius', 'width', 'min-width', 'padding'],
|
||||
{
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}
|
||||
),
|
||||
width: 24,
|
||||
'&:hover': {
|
||||
width: 52,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
},
|
||||
'& .FuseSidePanel-content': {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'& .FuseSidePanel-content': {
|
||||
overflow: 'hidden',
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['opacity'], {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
'& .FuseSidePanel-buttonWrapper': {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '12px 0',
|
||||
width: '100%',
|
||||
minWidth: 56,
|
||||
},
|
||||
|
||||
'& .FuseSidePanel-button': {
|
||||
padding: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transition: theme.transitions.create(['transform'], {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
'& .FuseSidePanel-mobileButton': {
|
||||
height: 40,
|
||||
position: 'absolute',
|
||||
zIndex: 99,
|
||||
bottom: 12,
|
||||
width: 24,
|
||||
borderRadius: 38,
|
||||
padding: 8,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
transition: theme.transitions.create(
|
||||
['background-color', 'border-radius', 'width', 'min-width', 'padding'],
|
||||
{
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}
|
||||
),
|
||||
'&:hover': {
|
||||
width: 52,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
'&.left': {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
paddingLeft: 4,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
'&.right': {
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
paddingRight: 4,
|
||||
right: 0,
|
||||
'& .FuseSidePanel-buttonIcon': {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function FuseSidePanel(props) {
|
||||
const [opened, setOpened] = useState(props.opened);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
function toggleOpened() {
|
||||
setOpened(!opened);
|
||||
}
|
||||
|
||||
function toggleMobileDrawer() {
|
||||
setMobileOpen(!mobileOpen);
|
||||
}
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Hidden lgDown>
|
||||
<Paper
|
||||
className={clsx(
|
||||
'FuseSidePanel-paper',
|
||||
props.className,
|
||||
opened ? 'opened' : 'closed',
|
||||
props.position,
|
||||
'shadow-lg'
|
||||
)}
|
||||
square
|
||||
>
|
||||
<FuseScrollbars className={clsx('content', 'FuseSidePanel-content')}>
|
||||
{props.children}
|
||||
</FuseScrollbars>
|
||||
|
||||
<div className="FuseSidePanel-buttonWrapper">
|
||||
<Tooltip
|
||||
title="Toggle side panel"
|
||||
placement={props.position === 'left' ? 'right' : 'right'}
|
||||
>
|
||||
<IconButton
|
||||
className="FuseSidePanel-button"
|
||||
onClick={toggleOpened}
|
||||
disableRipple
|
||||
size="large"
|
||||
>
|
||||
<FuseSvgIcon className="FuseSidePanel-buttonIcon">
|
||||
heroicons-outline:chevron-left
|
||||
</FuseSvgIcon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Paper>
|
||||
</Hidden>
|
||||
<Hidden lgUp>
|
||||
<SwipeableDrawer
|
||||
classes={{
|
||||
paper: clsx('FuseSidePanel-paper', props.className),
|
||||
}}
|
||||
anchor={props.position}
|
||||
open={mobileOpen}
|
||||
onOpen={(ev) => {}}
|
||||
onClose={toggleMobileDrawer}
|
||||
disableSwipeToOpen
|
||||
>
|
||||
<FuseScrollbars className={clsx('content', 'FuseSidePanel-content')}>
|
||||
{props.children}
|
||||
</FuseScrollbars>
|
||||
</SwipeableDrawer>
|
||||
|
||||
<Tooltip title="Hide side panel" placement={props.position === 'left' ? 'right' : 'right'}>
|
||||
<Fab
|
||||
className={clsx('FuseSidePanel-mobileButton', props.position)}
|
||||
onClick={toggleMobileDrawer}
|
||||
disableRipple
|
||||
>
|
||||
<FuseSvgIcon className="FuseSidePanel-buttonIcon">
|
||||
heroicons-outline:chevron-right
|
||||
</FuseSvgIcon>
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</Hidden>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
FuseSidePanel.propTypes = {};
|
||||
FuseSidePanel.defaultProps = {
|
||||
position: 'left',
|
||||
opened: true,
|
||||
};
|
||||
|
||||
export default memo(FuseSidePanel);
|
||||
1
src/@fuse/core/FuseSidePanel/index.js
Normal file
1
src/@fuse/core/FuseSidePanel/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSidePanel';
|
||||
26
src/@fuse/core/FuseSplashScreen/FuseSplashScreen.js
Normal file
26
src/@fuse/core/FuseSplashScreen/FuseSplashScreen.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { memo } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
function FuseSplashScreen() {
|
||||
return (
|
||||
<div id="fuse-splash-screen">
|
||||
<div className="logo">
|
||||
<img width="128" src="assets/images/logo/logo.svg" alt="logo" />
|
||||
</div>
|
||||
<Box
|
||||
id="spinner"
|
||||
sx={{
|
||||
'& > div': {
|
||||
backgroundColor: 'palette.secondary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="bounce1" />
|
||||
<div className="bounce2" />
|
||||
<div className="bounce3" />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FuseSplashScreen);
|
||||
1
src/@fuse/core/FuseSplashScreen/index.js
Normal file
1
src/@fuse/core/FuseSplashScreen/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSplashScreen';
|
||||
22
src/@fuse/core/FuseSuspense/FuseSuspense.js
Normal file
22
src/@fuse/core/FuseSuspense/FuseSuspense.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import FuseLoading from '@fuse/core/FuseLoading';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
/**
|
||||
* React Suspense defaults
|
||||
* For to Avoid Repetition
|
||||
*/ function FuseSuspense(props) {
|
||||
return <Suspense fallback={<FuseLoading {...props.loadingProps} />}>{props.children}</Suspense>;
|
||||
}
|
||||
|
||||
FuseSuspense.propTypes = {
|
||||
loadingProps: PropTypes.object,
|
||||
};
|
||||
|
||||
FuseSuspense.defaultProps = {
|
||||
loadingProps: {
|
||||
delay: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export default FuseSuspense;
|
||||
1
src/@fuse/core/FuseSuspense/index.js
Normal file
1
src/@fuse/core/FuseSuspense/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSuspense';
|
||||
83
src/@fuse/core/FuseSvgIcon/FuseSvgIcon.js
Normal file
83
src/@fuse/core/FuseSvgIcon/FuseSvgIcon.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box } from '@mui/system';
|
||||
import Icon from '@mui/material/Icon';
|
||||
|
||||
const Root = styled(Box)(({ theme, ...props }) => ({
|
||||
width: props.size,
|
||||
height: props.size,
|
||||
minWidth: props.size,
|
||||
minHeight: props.size,
|
||||
fontSize: props.size,
|
||||
lineHeight: props.size,
|
||||
color: {
|
||||
primary: theme.palette.primary.main,
|
||||
secondary: theme.palette.secondary.main,
|
||||
info: theme.palette.info.main,
|
||||
success: theme.palette.success.main,
|
||||
warning: theme.palette.warning.main,
|
||||
action: theme.palette.action.active,
|
||||
error: theme.palette.error.main,
|
||||
disabled: theme.palette.action.disabled,
|
||||
inherit: undefined,
|
||||
}[props.color],
|
||||
}));
|
||||
|
||||
const FuseSvgIcon = forwardRef((props, ref) => {
|
||||
const { children, size, sx, className, color } = props;
|
||||
const iconPath = props.children.replace(':', '.svg#');
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
{!props.children.includes(':') ? (
|
||||
<Icon ref={ref} {...props} />
|
||||
) : (
|
||||
<Root
|
||||
{...props}
|
||||
component="svg"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
className={clsx('shrink-0 fill-current ', className)}
|
||||
ref={ref}
|
||||
size={size}
|
||||
sx={sx}
|
||||
color={color}
|
||||
>
|
||||
<use xlinkHref={`assets/icons/${iconPath}`} />
|
||||
</Root>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[children, ref, className, size, sx, color, iconPath]
|
||||
);
|
||||
});
|
||||
|
||||
FuseSvgIcon.propTypes = {
|
||||
children: PropTypes.string,
|
||||
size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
sx: PropTypes.object,
|
||||
color: PropTypes.oneOf([
|
||||
'inherit',
|
||||
'disabled',
|
||||
'primary',
|
||||
'secondary',
|
||||
'action',
|
||||
'error',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
]),
|
||||
};
|
||||
|
||||
FuseSvgIcon.defaultProps = {
|
||||
children: '',
|
||||
size: 24,
|
||||
sx: {},
|
||||
color: 'inherit',
|
||||
};
|
||||
|
||||
export default FuseSvgIcon;
|
||||
1
src/@fuse/core/FuseSvgIcon/index.js
Normal file
1
src/@fuse/core/FuseSvgIcon/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseSvgIcon';
|
||||
23
src/@fuse/core/FuseTheme/FuseTheme.js
Normal file
23
src/@fuse/core/FuseTheme/FuseTheme.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { memo, useEffect, useLayoutEffect } from 'react';
|
||||
|
||||
const useEnhancedEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
||||
|
||||
function FuseTheme(props) {
|
||||
const { direction, theme } = props;
|
||||
const { mode } = theme.palette;
|
||||
|
||||
useEnhancedEffect(() => {
|
||||
document.body.dir = direction;
|
||||
}, [direction]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add(mode === 'light' ? 'light' : 'dark');
|
||||
document.body.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||
}, [mode]);
|
||||
|
||||
// console.warn('FuseTheme:: rendered',mainTheme);
|
||||
return <ThemeProvider theme={theme}>{props.children}</ThemeProvider>;
|
||||
}
|
||||
|
||||
export default memo(FuseTheme);
|
||||
1
src/@fuse/core/FuseTheme/index.js
Normal file
1
src/@fuse/core/FuseTheme/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseTheme';
|
||||
94
src/@fuse/core/FuseThemeSchemes/FuseThemeSchemes.js
Normal file
94
src/@fuse/core/FuseThemeSchemes/FuseThemeSchemes.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { memo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function SchemePreview({ theme, className, id, onSelect }) {
|
||||
const _theme = useTheme();
|
||||
const primaryColor = theme.palette.primary[500]
|
||||
? theme.palette.primary[500]
|
||||
: theme.palette.primary.main;
|
||||
const primaryColorContrast =
|
||||
theme.palette.primary.contrastText || _theme.palette.getContrastText(primaryColor);
|
||||
const secondaryColor = theme.palette.secondary[500]
|
||||
? theme.palette.secondary[500]
|
||||
: theme.palette.secondary.main;
|
||||
const secondaryColorContrast =
|
||||
theme.palette.secondary.contrastText || _theme.palette.getContrastText(secondaryColor);
|
||||
const backgroundColor = theme.palette.background.default;
|
||||
const backgroundColorContrast = _theme.palette.getContrastText(theme.palette.background.default);
|
||||
const paperColor = theme.palette.background.paper;
|
||||
const paperColorContrast = _theme.palette.getContrastText(theme.palette.background.paper);
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'mb-8')}>
|
||||
<button
|
||||
className={clsx(
|
||||
'w-full text-left rounded-6 relative font-500 shadow hover:shadow-md transition-shadow cursor-pointer overflow-hidden'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor,
|
||||
color: backgroundColorContrast,
|
||||
}}
|
||||
onClick={() => onSelect(theme)}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
className="w-full h-56 px-8 pt-8 relative"
|
||||
style={{
|
||||
backgroundColor: primaryColor,
|
||||
color: primaryColorContrast,
|
||||
}}
|
||||
>
|
||||
<span className="text-12 opacity-75">Header (Primary)</span>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-center w-20 h-20 rounded-full absolute bottom-0 right-0 -mb-10 shadow text-10 mr-4"
|
||||
style={{
|
||||
backgroundColor: secondaryColor,
|
||||
color: secondaryColorContrast,
|
||||
}}
|
||||
>
|
||||
<span className="opacity-75">S</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-8 pr-28 -mt-24 w-full">
|
||||
<div
|
||||
className="w-full h-96 rounded-4 relative shadow p-8"
|
||||
style={{
|
||||
backgroundColor: paperColor,
|
||||
color: paperColorContrast,
|
||||
}}
|
||||
>
|
||||
<span className="text-12 opacity-75">Paper</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 py-8 w-full">
|
||||
<span className="text-12 opacity-75">Background</span>
|
||||
</div>
|
||||
</button>
|
||||
<Typography className="font-semibold w-full text-center mt-12">{id}</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FuseThemeSchemes(props) {
|
||||
const { themes } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap w-full -mx-8">
|
||||
{Object.entries(themes)
|
||||
.filter(([key, val]) => !(key === 'mainThemeDark' || key === 'mainThemeLight'))
|
||||
.map(([key, val]) => (
|
||||
<div key={key} className="w-1/2 p-8">
|
||||
<SchemePreview id={key} theme={val} onSelect={() => props?.onSelect(val)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FuseThemeSchemes);
|
||||
1
src/@fuse/core/FuseThemeSchemes/index.js
Normal file
1
src/@fuse/core/FuseThemeSchemes/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseThemeSchemes';
|
||||
20
src/@fuse/core/NavLinkAdapter/NavLinkAdapter.js
Normal file
20
src/@fuse/core/NavLinkAdapter/NavLinkAdapter.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { NavLink as BaseNavLink } from 'react-router-dom';
|
||||
|
||||
const NavLinkAdapter = forwardRef(({ activeClassName, activeStyle, ...props }, ref) => {
|
||||
return (
|
||||
<BaseNavLink
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={({ isActive }) =>
|
||||
[props.className, isActive ? activeClassName : null].filter(Boolean).join(' ')
|
||||
}
|
||||
style={({ isActive }) => ({
|
||||
...props.style,
|
||||
...(isActive ? activeStyle : null),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default NavLinkAdapter;
|
||||
1
src/@fuse/core/NavLinkAdapter/index.js
Normal file
1
src/@fuse/core/NavLinkAdapter/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './NavLinkAdapter';
|
||||
1
src/@fuse/core/withRouter/index.js
Normal file
1
src/@fuse/core/withRouter/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './withRouter';
|
||||
11
src/@fuse/core/withRouter/withRouter.js
Normal file
11
src/@fuse/core/withRouter/withRouter.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
function withRouter(Child) {
|
||||
return (props) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
return <Child {...props} navigate={navigate} location={location} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter;
|
||||
1
src/@fuse/core/withRouterAndRef/index.js
Normal file
1
src/@fuse/core/withRouterAndRef/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './withRouterAndRef';
|
||||
16
src/@fuse/core/withRouterAndRef/withRouterAndRef.js
Normal file
16
src/@fuse/core/withRouterAndRef/withRouterAndRef.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component, forwardRef } from 'react';
|
||||
import withRouter from '@fuse/core/withRouter';
|
||||
|
||||
const withRouterAndRef = (WrappedComponent) => {
|
||||
class InnerComponentWithRef extends Component {
|
||||
render() {
|
||||
const { forwardRef: _forwardRef, ...rest } = this.props;
|
||||
return <WrappedComponent {...rest} ref={_forwardRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ComponentWithRouter = withRouter(InnerComponentWithRef, { withRef: true });
|
||||
return forwardRef((props, ref) => <ComponentWithRouter {...props} forwardRef={ref} />);
|
||||
};
|
||||
|
||||
export default withRouterAndRef;
|
||||
318
src/@fuse/default-settings/FuseDefaultSettings.js
Normal file
318
src/@fuse/default-settings/FuseDefaultSettings.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import { fuseDark } from '@fuse/colors';
|
||||
import { lightBlue, red } from '@mui/material/colors';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import qs from 'qs';
|
||||
|
||||
const defaultTheme = {
|
||||
palette: {
|
||||
mode: 'light',
|
||||
text: {
|
||||
primary: 'rgb(17, 24, 39)',
|
||||
secondary: 'rgb(107, 114, 128)',
|
||||
disabled: 'rgb(149, 156, 169)',
|
||||
},
|
||||
common: {
|
||||
black: 'rgb(17, 24, 39)',
|
||||
white: 'rgb(255, 255, 255)',
|
||||
},
|
||||
primary: {
|
||||
light: '#bec1c5',
|
||||
main: '#252f3e',
|
||||
dark: '#0d121b',
|
||||
contrastDefaultColor: 'light',
|
||||
},
|
||||
secondary: {
|
||||
light: '#bdf2fa',
|
||||
main: '#22d3ee',
|
||||
dark: '#0cb7e2',
|
||||
},
|
||||
background: {
|
||||
paper: '#FFFFFF',
|
||||
default: '#f6f7f9',
|
||||
},
|
||||
error: {
|
||||
light: '#ffcdd2',
|
||||
main: '#f44336',
|
||||
dark: '#b71c1c',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* SETTINGS DEFAULTS
|
||||
*/
|
||||
export const defaultSettings = {
|
||||
customScrollbars: true,
|
||||
direction: 'ltr',
|
||||
theme: {
|
||||
main: defaultTheme,
|
||||
navbar: defaultTheme,
|
||||
toolbar: defaultTheme,
|
||||
footer: defaultTheme,
|
||||
},
|
||||
};
|
||||
|
||||
export function getParsedQuerySettings() {
|
||||
const parsedQueryString = qs.parse(window.location.search, { ignoreQueryPrefix: true });
|
||||
|
||||
if (parsedQueryString && parsedQueryString.defaultSettings) {
|
||||
return JSON.parse(parsedQueryString.defaultSettings);
|
||||
}
|
||||
return {};
|
||||
|
||||
// Generating route params from settings
|
||||
/* const settings = qs.stringify({
|
||||
defaultSettings: JSON.stringify(defaultSettings, {strictNullHandling: true})
|
||||
});
|
||||
console.info(settings); */
|
||||
}
|
||||
|
||||
/**
|
||||
* THEME DEFAULTS
|
||||
*/
|
||||
export const defaultThemeOptions = {
|
||||
typography: {
|
||||
fontFamily: ['Inter var', 'Roboto', '"Helvetica"', 'Arial', 'sans-serif'].join(','),
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
fontWeightMedium: 500,
|
||||
},
|
||||
components: {
|
||||
MuiDateTimePicker: {
|
||||
defaultProps: {
|
||||
PopperProps: { className: 'z-9999' },
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
defaultProps: {
|
||||
enableColorOnDark: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButtonBase: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
variant: 'text',
|
||||
color: 'inherit',
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
// lineHeight: 1,
|
||||
},
|
||||
sizeMedium: {
|
||||
borderRadius: 20,
|
||||
height: 40,
|
||||
minHeight: 40,
|
||||
maxHeight: 40,
|
||||
},
|
||||
sizeSmall: {
|
||||
borderRadius: '15px',
|
||||
},
|
||||
sizeLarge: {
|
||||
borderRadius: '28px',
|
||||
},
|
||||
contained: {
|
||||
boxShadow: 'none',
|
||||
'&:hover, &:focus': {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButtonGroup: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
styleOverrides: {
|
||||
contained: {
|
||||
borderRadius: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
rounded: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiSelect: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
minHeight: 40,
|
||||
lineHeight: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFilledInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 4,
|
||||
'&:before, &:after': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSlider: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiRadio: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiSwitch: {
|
||||
defaultProps: {
|
||||
color: 'secondary',
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
variants: [
|
||||
{
|
||||
props: { color: 'text.secondary' },
|
||||
style: {
|
||||
color: 'text.secondary',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mustHaveThemeOptions = {
|
||||
typography: {
|
||||
htmlFontSize: 10,
|
||||
fontSize: 14,
|
||||
body1: {
|
||||
fontSize: '1.4rem',
|
||||
},
|
||||
body2: {
|
||||
fontSize: '1.4rem',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultThemes = {
|
||||
default: {
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: fuseDark,
|
||||
secondary: {
|
||||
light: lightBlue[400],
|
||||
main: lightBlue[600],
|
||||
dark: lightBlue[700],
|
||||
},
|
||||
error: red,
|
||||
},
|
||||
status: {
|
||||
danger: 'orange',
|
||||
},
|
||||
},
|
||||
defaultDark: {
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: fuseDark,
|
||||
secondary: {
|
||||
light: lightBlue[400],
|
||||
main: lightBlue[600],
|
||||
dark: lightBlue[700],
|
||||
},
|
||||
error: red,
|
||||
},
|
||||
status: {
|
||||
danger: 'orange',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function extendThemeWithMixins(obj) {
|
||||
const theme = createTheme(obj);
|
||||
return {
|
||||
border: (width = 1) => ({
|
||||
borderWidth: width,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
borderLeft: (width = 1) => ({
|
||||
borderLeftWidth: width,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
borderRight: (width = 1) => ({
|
||||
borderRightWidth: width,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
borderTop: (width = 1) => ({
|
||||
borderTopWidth: width,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
borderBottom: (width = 1) => ({
|
||||
borderBottomWidth: width,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
};
|
||||
}
|
||||
1
src/@fuse/default-settings/index.js
Normal file
1
src/@fuse/default-settings/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './FuseDefaultSettings';
|
||||
8
src/@fuse/hooks/index.js
Normal file
8
src/@fuse/hooks/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as useForm } from './useForm';
|
||||
export { default as useDebounce } from './useDebounce';
|
||||
export { default as useTimeout } from './useTimeout';
|
||||
export { default as usePrevious } from './usePrevious';
|
||||
export { default as useUpdateEffect } from './useUpdateEffect';
|
||||
export { default as useDeepCompareEffect } from './useDeepCompareEffect';
|
||||
export { default as useThemeMediaQuery } from './useThemeMediaQuery';
|
||||
export { default as useEventListener } from './useEventListener';
|
||||
8
src/@fuse/hooks/useDebounce.js
Normal file
8
src/@fuse/hooks/useDebounce.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import _ from '@lodash';
|
||||
import { useRef } from 'react';
|
||||
|
||||
function useDebounce(func, wait, options) {
|
||||
return useRef(_.debounce(func, wait, options)).current;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
47
src/@fuse/hooks/useDeepCompareEffect.js
Normal file
47
src/@fuse/hooks/useDeepCompareEffect.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import deepEqual from 'lodash/isEqual';
|
||||
|
||||
/**
|
||||
* https://github.com/kentcdodds/use-deep-compare-effect
|
||||
*/
|
||||
function checkDeps(deps) {
|
||||
if (!deps || !deps.length) {
|
||||
throw new Error(
|
||||
'useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.'
|
||||
);
|
||||
}
|
||||
if (deps.every(isPrimitive)) {
|
||||
throw new Error(
|
||||
'useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isPrimitive(val) {
|
||||
return val == null || /^[sbn]/.test(typeof val);
|
||||
}
|
||||
|
||||
function useDeepCompareMemoize(value) {
|
||||
const ref = useRef();
|
||||
|
||||
if (!deepEqual(value, ref.current)) {
|
||||
ref.current = value;
|
||||
}
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function useDeepCompareEffect(callback, dependencies) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
checkDeps(dependencies);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
useEffect(callback, useDeepCompareMemoize(dependencies));
|
||||
}
|
||||
|
||||
export function useDeepCompareEffectNoCheck(callback, dependencies) {
|
||||
// eslint-disable-next-line
|
||||
useEffect(callback, useDeepCompareMemoize(dependencies));
|
||||
}
|
||||
|
||||
export default useDeepCompareEffect;
|
||||
42
src/@fuse/hooks/useEventListener.js
Normal file
42
src/@fuse/hooks/useEventListener.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* https://usehooks.com/useEventListener/
|
||||
*/
|
||||
function useEventListener(eventName, handler, element = window) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef();
|
||||
|
||||
// Update ref.current value if handler changes.
|
||||
// This allows our effect below to always get latest handler ...
|
||||
// ... without us needing to pass it in effect deps array ...
|
||||
// ... and potentially cause effect to re-run every render.
|
||||
useEffect(() => {
|
||||
savedHandler.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Make sure element supports addEventListener
|
||||
// On
|
||||
const isSupported = element && element.addEventListener;
|
||||
if (!isSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create event listener that calls handler function stored in ref
|
||||
const eventListener = (event) => savedHandler.current(event);
|
||||
|
||||
// Add event listener
|
||||
element.addEventListener(eventName, eventListener);
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
element.removeEventListener(eventName, eventListener);
|
||||
};
|
||||
},
|
||||
[eventName, element] // Re-run if eventName or element changes
|
||||
);
|
||||
}
|
||||
|
||||
export default useEventListener;
|
||||
50
src/@fuse/hooks/useForm.js
Normal file
50
src/@fuse/hooks/useForm.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import _ from '@lodash';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
function useForm(initialState, onSubmit) {
|
||||
const [form, setForm] = useState(initialState);
|
||||
|
||||
const handleChange = useCallback((event) => {
|
||||
event.persist();
|
||||
setForm((_form) =>
|
||||
_.setIn(
|
||||
{ ..._form },
|
||||
event.target.name,
|
||||
event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
if (!_.isEqual(initialState, form)) {
|
||||
setForm(initialState);
|
||||
}
|
||||
}, [form, initialState]);
|
||||
|
||||
const setInForm = useCallback((name, value) => {
|
||||
setForm((_form) => _.setIn(_form, name, value));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (onSubmit) {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
||||
return {
|
||||
form,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
resetForm,
|
||||
setForm,
|
||||
setInForm,
|
||||
};
|
||||
}
|
||||
|
||||
export default useForm;
|
||||
17
src/@fuse/hooks/usePrevious.js
Normal file
17
src/@fuse/hooks/usePrevious.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function usePrevious(value) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef();
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]); // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export default usePrevious;
|
||||
33
src/@fuse/hooks/useThemeMediaQuery.js
Normal file
33
src/@fuse/hooks/useThemeMediaQuery.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
function useThemeMediaQuery(themeCallbackFunc) {
|
||||
const theme = useTheme();
|
||||
|
||||
const query = themeCallbackFunc(theme).replace('@media ', '');
|
||||
|
||||
const [matches, setMatches] = useState(getMatches(query));
|
||||
|
||||
function getMatches(q) {
|
||||
return window.matchMedia(q).matches;
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const mediaQuery = window.matchMedia(query);
|
||||
// Update the state with the current value
|
||||
setMatches(getMatches(query));
|
||||
// Create an event listener
|
||||
const handler = (event) => setMatches(event.matches);
|
||||
// Attach the event listener to know when the matches value changes
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
// Remove the event listener on cleanup
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
},
|
||||
[query] // Empty array ensures effect is only run on mount and unmount
|
||||
);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export default useThemeMediaQuery;
|
||||
25
src/@fuse/hooks/useTimeout.js
Normal file
25
src/@fuse/hooks/useTimeout.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function useTimeout(callback, delay) {
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
|
||||
if (delay && callback && typeof callback === 'function') {
|
||||
timer = setTimeout(callbackRef.current, delay || 0);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [callback, delay]);
|
||||
}
|
||||
|
||||
export default useTimeout;
|
||||
16
src/@fuse/hooks/useUpdateEffect.js
Normal file
16
src/@fuse/hooks/useUpdateEffect.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useUpdateEffect = (effect, deps) => {
|
||||
const isInitialMount = useRef(true);
|
||||
// eslint-disable-next-line
|
||||
useEffect(
|
||||
isInitialMount.current
|
||||
? () => {
|
||||
isInitialMount.current = false;
|
||||
}
|
||||
: effect,
|
||||
deps
|
||||
);
|
||||
};
|
||||
|
||||
export default useUpdateEffect;
|
||||
37
src/@fuse/tailwind/plugins/icon-size.js
Normal file
37
src/@fuse/tailwind/plugins/icon-size.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
const iconSize = plugin(
|
||||
({ addUtilities, theme, e, variants }) => {
|
||||
const values = theme('iconSize');
|
||||
|
||||
addUtilities(
|
||||
Object.entries(values).map(([key, value]) => ({
|
||||
[`.${e(`icon-size-${key}`)}`]: {
|
||||
width: value,
|
||||
height: value,
|
||||
minWidth: value,
|
||||
minHeight: value,
|
||||
fontSize: value,
|
||||
lineHeight: value,
|
||||
[`svg`]: {
|
||||
width: value,
|
||||
height: value,
|
||||
},
|
||||
},
|
||||
})),
|
||||
variants('iconSize')
|
||||
);
|
||||
},
|
||||
{
|
||||
theme: {
|
||||
iconSize: (theme) => ({
|
||||
...theme('spacing'),
|
||||
}),
|
||||
},
|
||||
variants: {
|
||||
iconSize: ['responsive'],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = iconSize;
|
||||
391
src/@fuse/utils/FuseUtils.js
Normal file
391
src/@fuse/utils/FuseUtils.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import _ from '@lodash';
|
||||
import * as colors from '@mui/material/colors';
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
_getEventListByName(eventName) {
|
||||
if (typeof this.events[eventName] === 'undefined') {
|
||||
this.events[eventName] = new Set();
|
||||
}
|
||||
return this.events[eventName];
|
||||
}
|
||||
|
||||
on(eventName, fn) {
|
||||
this._getEventListByName(eventName).add(fn);
|
||||
}
|
||||
|
||||
once(eventName, fn) {
|
||||
const self = this;
|
||||
|
||||
const onceFn = (...args) => {
|
||||
self.removeListener(eventName, onceFn);
|
||||
fn.apply(self, args);
|
||||
};
|
||||
this.on(eventName, onceFn);
|
||||
}
|
||||
|
||||
emit(eventName, ...args) {
|
||||
this._getEventListByName(eventName).forEach(
|
||||
// eslint-disable-next-line func-names
|
||||
function (fn) {
|
||||
fn.apply(this, args);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
removeListener(eventName, fn) {
|
||||
this._getEventListByName(eventName).delete(fn);
|
||||
}
|
||||
}
|
||||
|
||||
class FuseUtils {
|
||||
static filterArrayByString(mainArr, searchText) {
|
||||
if (searchText === '') {
|
||||
return mainArr;
|
||||
}
|
||||
|
||||
searchText = searchText.toLowerCase();
|
||||
|
||||
return mainArr.filter((itemObj) => this.searchInObj(itemObj, searchText));
|
||||
}
|
||||
|
||||
static searchInObj(itemObj, searchText) {
|
||||
if (!itemObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const propArray = Object.keys(itemObj);
|
||||
|
||||
for (let i = 0; i < propArray.length; i += 1) {
|
||||
const prop = propArray[i];
|
||||
const value = itemObj[prop];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (this.searchInString(value, searchText)) {
|
||||
return true;
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
if (this.searchInArray(value, searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (this.searchInObj(value, searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static searchInArray(arr, searchText) {
|
||||
arr.forEach((value) => {
|
||||
if (typeof value === 'string') {
|
||||
if (this.searchInString(value, searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (this.searchInObj(value, searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
static searchInString(value, searchText) {
|
||||
return value.toLowerCase().includes(searchText);
|
||||
}
|
||||
|
||||
static generateGUID() {
|
||||
function S4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
return S4() + S4();
|
||||
}
|
||||
|
||||
static toggleInArray(item, array) {
|
||||
if (array.indexOf(item) === -1) {
|
||||
array.push(item);
|
||||
} else {
|
||||
array.splice(array.indexOf(item), 1);
|
||||
}
|
||||
}
|
||||
|
||||
static handleize(text) {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/\W+/g, '') // Remove all non-word chars
|
||||
.replace(/--+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, ''); // Trim - from end of text
|
||||
}
|
||||
|
||||
static setRoutes(config, defaultAuth) {
|
||||
let routes = [...config.routes];
|
||||
|
||||
routes = routes.map((route) => {
|
||||
let auth = config.auth || config.auth === null ? config.auth : defaultAuth || null;
|
||||
auth = route.auth || route.auth === null ? route.auth : auth;
|
||||
const settings = _.merge({}, config.settings, route.settings);
|
||||
|
||||
return {
|
||||
...route,
|
||||
settings,
|
||||
auth,
|
||||
};
|
||||
});
|
||||
|
||||
return [...routes];
|
||||
}
|
||||
|
||||
static generateRoutesFromConfigs(configs, defaultAuth) {
|
||||
let allRoutes = [];
|
||||
configs.forEach((config) => {
|
||||
allRoutes = [...allRoutes, ...this.setRoutes(config, defaultAuth)];
|
||||
});
|
||||
return allRoutes;
|
||||
}
|
||||
|
||||
static findById(obj, id) {
|
||||
let i;
|
||||
let childObj;
|
||||
let result;
|
||||
|
||||
if (id === obj.id) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
for (i = 0; i < Object.keys(obj).length; i += 1) {
|
||||
childObj = obj[Object.keys(obj)[i]];
|
||||
|
||||
if (typeof childObj === 'object') {
|
||||
result = this.findById(childObj, id);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getFlatNavigation(navigationItems, flatNavigation = []) {
|
||||
for (let i = 0; i < navigationItems.length; i += 1) {
|
||||
const navItem = navigationItems[i];
|
||||
|
||||
if (navItem.type === 'item') {
|
||||
flatNavigation.push({
|
||||
id: navItem.id,
|
||||
title: navItem.title,
|
||||
type: navItem.type,
|
||||
icon: navItem.icon || false,
|
||||
url: navItem.url,
|
||||
auth: navItem.auth || null,
|
||||
});
|
||||
}
|
||||
|
||||
if (navItem.type === 'collapse' || navItem.type === 'group') {
|
||||
if (navItem.children) {
|
||||
this.getFlatNavigation(navItem.children, flatNavigation);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flatNavigation;
|
||||
}
|
||||
|
||||
static randomMatColor(hue) {
|
||||
hue = hue || '400';
|
||||
const mainColors = [
|
||||
'red',
|
||||
'pink',
|
||||
'purple',
|
||||
'deepPurple',
|
||||
'indigo',
|
||||
'blue',
|
||||
'lightBlue',
|
||||
'cyan',
|
||||
'teal',
|
||||
'green',
|
||||
'lightGreen',
|
||||
'lime',
|
||||
'yellow',
|
||||
'amber',
|
||||
'orange',
|
||||
'deepOrange',
|
||||
];
|
||||
const randomColor = mainColors[Math.floor(Math.random() * mainColors.length)];
|
||||
return colors[randomColor][hue];
|
||||
}
|
||||
|
||||
static difference(object, base) {
|
||||
function changes(_object, _base) {
|
||||
return _.transform(_object, (result, value, key) => {
|
||||
if (!_.isEqual(value, _base[key])) {
|
||||
result[key] =
|
||||
_.isObject(value) && _.isObject(_base[key]) ? changes(value, _base[key]) : value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changes(object, base);
|
||||
}
|
||||
|
||||
static EventEmitter = EventEmitter;
|
||||
|
||||
static updateNavItem(nav, id, item) {
|
||||
return nav.map((_item) => {
|
||||
if (_item.id === id) {
|
||||
return _.merge({}, _item, item);
|
||||
}
|
||||
|
||||
if (_item.children) {
|
||||
return _.merge({}, _item, {
|
||||
children: this.updateNavItem(_item.children, id, item),
|
||||
});
|
||||
}
|
||||
|
||||
return _.merge({}, _item);
|
||||
});
|
||||
}
|
||||
|
||||
static removeNavItem(nav, id) {
|
||||
return nav
|
||||
.map((_item) => {
|
||||
if (_item.id === id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_item.children) {
|
||||
return _.merge({}, _.omit(_item, ['children']), {
|
||||
children: this.removeNavItem(_item.children, id),
|
||||
});
|
||||
}
|
||||
|
||||
return _.merge({}, _item);
|
||||
})
|
||||
.filter((s) => s);
|
||||
}
|
||||
|
||||
static prependNavItem(nav, item, parentId) {
|
||||
if (!parentId) {
|
||||
return [item, ...nav];
|
||||
}
|
||||
|
||||
return nav.map((_item) => {
|
||||
if (_item.id === parentId && _item.children) {
|
||||
return {
|
||||
..._item,
|
||||
children: [item, ..._item.children],
|
||||
};
|
||||
}
|
||||
|
||||
if (_item.children) {
|
||||
return _.merge({}, _item, {
|
||||
children: this.prependNavItem(_item.children, item, parentId),
|
||||
});
|
||||
}
|
||||
|
||||
return _.merge({}, _item);
|
||||
});
|
||||
}
|
||||
|
||||
static appendNavItem(nav, item, parentId) {
|
||||
if (!parentId) {
|
||||
return [...nav, item];
|
||||
}
|
||||
|
||||
return nav.map((_item) => {
|
||||
if (_item.id === parentId && _item.children) {
|
||||
return {
|
||||
..._item,
|
||||
children: [..._item.children, item],
|
||||
};
|
||||
}
|
||||
|
||||
if (_item.children) {
|
||||
return _.merge({}, _item, {
|
||||
children: this.appendNavItem(_item.children, item, parentId),
|
||||
});
|
||||
}
|
||||
|
||||
return _.merge({}, _item);
|
||||
});
|
||||
}
|
||||
|
||||
static hasPermission(authArr, userRole) {
|
||||
/**
|
||||
* If auth array is not defined
|
||||
* Pass and allow
|
||||
*/
|
||||
if (authArr === null || authArr === undefined) {
|
||||
// console.info("auth is null || undefined:", authArr);
|
||||
return true;
|
||||
}
|
||||
if (authArr.length === 0) {
|
||||
/**
|
||||
* if auth array is empty means,
|
||||
* allow only user role is guest (null or empty[])
|
||||
*/
|
||||
// console.info("auth is empty[]:", authArr);
|
||||
return !userRole || userRole.length === 0;
|
||||
}
|
||||
/**
|
||||
* Check if user has grants
|
||||
*/
|
||||
// console.info("auth arr:", authArr);
|
||||
/*
|
||||
Check if user role is array,
|
||||
*/
|
||||
if (userRole && Array.isArray(userRole)) {
|
||||
return authArr.some((r) => userRole.indexOf(r) >= 0);
|
||||
}
|
||||
|
||||
/*
|
||||
Check if user role is string,
|
||||
*/
|
||||
return authArr.includes(userRole);
|
||||
}
|
||||
|
||||
static filterRecursive(data, predicate) {
|
||||
// if no data is sent in, return null, otherwise transform the data
|
||||
return !data
|
||||
? null
|
||||
: data.reduce((list, entry) => {
|
||||
let clone = null;
|
||||
if (predicate(entry)) {
|
||||
// if the object matches the filter, clone it as it is
|
||||
clone = { ...entry };
|
||||
}
|
||||
if (entry.children != null) {
|
||||
// if the object has childrens, filter the list of children
|
||||
const children = this.filterRecursive(entry.children, predicate);
|
||||
if (children.length > 0) {
|
||||
// if any of the children matches, clone the parent object, overwrite
|
||||
// the children list with the filtered list
|
||||
clone = { ...entry, children };
|
||||
}
|
||||
}
|
||||
|
||||
// if there's a cloned object, push it to the output list
|
||||
if (clone) {
|
||||
list.push(clone);
|
||||
}
|
||||
return list;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
export default FuseUtils;
|
||||
1
src/@fuse/utils/index.js
Normal file
1
src/@fuse/utils/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FuseUtils';
|
||||
3
src/@history/@history.js
Normal file
3
src/@history/@history.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as history from 'history';
|
||||
|
||||
export default history.createBrowserHistory();
|
||||
1
src/@history/index.js
Normal file
1
src/@history/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './@history';
|
||||
17
src/@lodash/@lodash.js
Normal file
17
src/@lodash/@lodash.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import __ from 'lodash';
|
||||
|
||||
/**
|
||||
* You can extend Lodash with mixins
|
||||
* And use it as below
|
||||
* import _ from '@lodash'
|
||||
*/
|
||||
const _ = __.runInContext();
|
||||
|
||||
_.mixin({
|
||||
// Immutable Set for setting state
|
||||
setIn: (state, name, value) => {
|
||||
return _.setWith(_.clone(state), name, value, _.clone);
|
||||
},
|
||||
});
|
||||
|
||||
export default _;
|
||||
1
src/@lodash/index.js
Normal file
1
src/@lodash/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './@lodash';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user