Add mobile support for mindmap list.

This commit is contained in:
Gonzalo Martinez 2023-01-05 00:48:01 +00:00 committed by Paulo Veiga
parent 82dcd34dc5
commit 5f67aa8c5c
42 changed files with 1031 additions and 415 deletions

View File

@ -76,12 +76,12 @@ export class DefaultWidgetManager extends WidgetManager {
topic.closeEditors();
}
static useCreate(): [boolean, Element | undefined, DefaultWidgetManager] {
static useCreate(): [boolean, (boolean) => void, Element | undefined, DefaultWidgetManager] {
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverTarget, setPopoverTarget] = useState(undefined);
const widgetManager = useRef(new DefaultWidgetManager(setPopoverOpen, setPopoverTarget));
return [popoverOpen, popoverTarget, widgetManager.current];
return [popoverOpen, setPopoverOpen, popoverTarget, widgetManager.current];
}
}

View File

@ -34,10 +34,6 @@ const SaveAndDelete = (props: {
<FormattedMessage id="action.accept" defaultMessage="Accept" />
</Button>
<Button color="primary" variant="outlined" onClick={props.closeModal}>
<FormattedMessage id="action.cancel" defaultMessage="Cancel" />
</Button>
{props.model.getValue() && props.model.getValue().trim() !== '' && (
<IconButton
onClick={() => {

View File

@ -59,7 +59,7 @@ const TopicLink = (props: {
const isValidUrl = !url || checkURL(url);
return (
<Box display="flex" sx={{ p: 1 }}>
<Box sx={{ p: 1 }}>
<Input
autoFocus
error={!isValidUrl}
@ -87,8 +87,9 @@ const TopicLink = (props: {
</Link>
),
}}
sx={{ pr: 1 }}
margin="dense"
></Input>
<br />
<SaveAndDelete
model={props.urlModel}
closeModal={props.closeModal}

View File

@ -29,9 +29,7 @@ import {
} from '@wisemapping/mindplot';
import I18nMsg from '../classes/i18n-msg';
import { theme as defaultEditorTheme } from '../theme';
// eslint-disable-next-line no-restricted-imports
import ThemeProvider from '@mui/material/styles/ThemeProvider';
import { Theme } from '@mui/material/styles';
import { Notifier } from './warning-dialog/styled';
import WarningDialog from './warning-dialog';
@ -42,6 +40,9 @@ import { ToolbarActionType } from './toolbar/ToolbarActionType';
import MapInfo from '../classes/model/map-info';
import EditorToolbar from './editor-toolbar';
import ZoomPanel from './zoom-panel';
import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box';
import CloseIcon from '@mui/icons-material/Close';
import { SpinnerCentered } from './style';
export type EditorOptions = {
@ -66,7 +67,6 @@ const Editor = ({
options,
persistenceManager,
onAction,
theme,
accountConfiguration,
}: EditorProps): ReactElement => {
const [model, setModel] = useState<Model | undefined>();
@ -75,8 +75,8 @@ const Editor = ({
// This is required to redraw in case of chansges in the canvas...
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [canvasUpdate, setCanvasUpdate] = useState<number>();
const editorTheme: Theme = theme ? theme : defaultEditorTheme;
const [popoverOpen, popoverTarget, widgetManager] = DefaultWidgetManager.useCreate();
const [popoverOpen, setPopoverOpen, popoverTarget, widgetManager] =
DefaultWidgetManager.useCreate();
const capability = new Capability(options.mode, mapInfo.isLocked());
useEffect(() => {
@ -108,59 +108,62 @@ const Editor = ({
const locale = options.locale;
const msg = I18nMsg.loadLocaleData(locale);
return (
<ThemeProvider theme={editorTheme}>
<IntlProvider locale={locale} messages={msg}>
<AppBar
model={model}
mapInfo={mapInfo}
capability={capability}
onAction={onAction}
accountConfig={accountConfiguration}
/>
<IntlProvider locale={locale} messages={msg}>
<AppBar
model={model}
mapInfo={mapInfo}
capability={capability}
onAction={onAction}
accountConfig={accountConfiguration}
/>
<Popover
id="popover"
open={popoverOpen}
anchorEl={popoverTarget}
onClose={widgetManager.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
{widgetManager.getEditorContent()}
</Popover>
<Popover
id="popover"
open={popoverOpen}
anchorEl={popoverTarget}
onClose={widgetManager.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Box alignItems={'end'}>
<IconButton onClick={() => setPopoverOpen(false)} aria-label={'Close'}>
<CloseIcon />
</IconButton>
</Box>
{widgetManager.getEditorContent()}
</Popover>
<EditorToolbar model={model} capability={capability} />
<ZoomPanel model={model} capability={capability} />
<EditorToolbar model={model} capability={capability} />
<ZoomPanel model={model} capability={capability} />
<mindplot-component
ref={mindplotRef}
id="mindmap-comp"
mode={options.mode}
locale={options.locale}
zoom={options.zoom}
/>
<mindplot-component
ref={mindplotRef}
id="mindmap-comp"
mode={options.mode}
locale={options.locale}
zoom={options.zoom}
/>
<Notifier id="headerNotifier" />
<WarningDialog
capability={capability}
message={mapInfo.isLocked() ? mapInfo.getLockedMessage() : ''}
/>
<Notifier id="headerNotifier" />
<WarningDialog
capability={capability}
message={mapInfo.isLocked() ? mapInfo.getLockedMessage() : ''}
/>
{!model?.isMapLoadded() && (
<SpinnerCentered>
<Vortex
visible={true}
height="160"
width="160"
ariaLabel="vortex-loading"
colors={['#ffde1a', '#ffce00', '#ffa700', '#ff8d00', '#ff7400', '#ffde1a']}
/>
</SpinnerCentered>
)}
</IntlProvider>
</ThemeProvider>
{!model?.isMapLoadded() && (
<SpinnerCentered>
<Vortex
visible={true}
height="160"
width="160"
ariaLabel="vortex-loading"
colors={['#ffde1a', '#ffce00', '#ffa700', '#ff8d00', '#ff7400', '#ffde1a']}
/>
</SpinnerCentered>
)}
</IntlProvider>
);
};
export default Editor;

View File

@ -25,6 +25,7 @@ import '../app-bar/styles.css';
import Box from '@mui/material/Box';
import ToolbarPosition from '../../classes/model/toolbar-position';
import ActionConfig from '../../classes/action/action-config';
import CloseIcon from '@mui/icons-material/Close';
/**
* Common button
@ -106,7 +107,11 @@ export const ToolbarSubmenu = (props: {
}}
>
<ToolbarButtonOption
configuration={{ ...props.configuration, onClick: () => setOpen(true) }}
configuration={{
...props.configuration,
onClick: () => setOpen(true),
selected: () => open,
}}
/>
<Popover
role="submenu"
@ -125,6 +130,13 @@ export const ToolbarSubmenu = (props: {
}}
elevation={props.elevation}
>
{props.configuration.useClickToClose && (
<Box alignItems={'end'}>
<IconButton onClick={() => setOpen(false)} aria-label={'Close'}>
<CloseIcon />
</IconButton>
</Box>
)}
<div style={{ display: 'flex' }} onScroll={(e) => e.stopPropagation()}>
{props.configuration.options?.map((o, i) => {
if (o?.visible === false) {

View File

@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import React from 'react';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { render, fireEvent, waitFor, screen, findByLabelText } from '@testing-library/react';
import ThreeDRotation from '@mui/icons-material/ThreeDRotation';
import Toolbar, {
ToolbarButtonOption,
@ -80,6 +80,12 @@ const iconFunctionConfig: ActionConfig = {
onClick: jest.fn(),
};
const submenuOnClickConfig: ActionConfig = {
icon: <ThreeDRotation></ThreeDRotation>,
useClickToClose: true,
options: [config, null, config, null],
};
afterEach(() => {
jest.clearAllMocks();
});
@ -192,6 +198,7 @@ describe('Editor Toolbar Submenu', () => {
const item = screen.getByRole('menuitem');
fireEvent.mouseOver(item);
const clickeableDiv = await screen.findByTestId('custom-render-div');
expect(screen.queryByRole('submenu')).toBeTruthy();
fireEvent.click(clickeableDiv);
@ -206,6 +213,28 @@ describe('Editor Toolbar Submenu', () => {
expect(screen.queryByRole('submenu')).toBeFalsy();
});
it('Given a useClickToOpen configuration when mouse is over, not shows a submenu ', async () => {
render(<ToolbarSubmenu configuration={submenuOnClickConfig}></ToolbarSubmenu>);
const item = screen.getByRole('menuitem');
fireEvent.mouseOver(item);
expect(screen.queryByRole('submenu')).toBeFalsy();
});
it('Given a useClickToOpen configuration when click, shows a submenu with close button', async () => {
render(<ToolbarSubmenu configuration={submenuOnClickConfig}></ToolbarSubmenu>);
const item = screen.getByRole('button');
fireEvent.click(item);
await screen.findByRole('submenu');
const closeButton = await screen.findByLabelText('Close');
fireEvent.click(closeButton);
expect(screen.queryByRole('submenu')).toBeFalsy();
});
});
describe('toolbar menu item', () => {

View File

@ -40,6 +40,7 @@
"!@mui/material/test-utils/*"
]
}
]
],
"react/no-unknown-property": ["error", { "ignore": ["css"] }]
}
}

View File

@ -1,6 +1,6 @@
context('Editor Page', () => {
beforeEach(() => {
cy.visit('c/maps/11/edit');
cy.visit('/c/maps/11/edit');
});
it('page loaded', () => {

View File

@ -7,3 +7,43 @@ context('Maps Page', () => {
// cy.matchImageSnapshot('maps');
});
});
context('iphone-5 resolution', () => {
beforeEach(() => {
cy.viewport('iphone-5');
cy.visit('/c/maps', {
onLoad: (win) => {
win.onerror = null;
},
});
});
it('Displays mobile menu button', () => {
cy.get('#open-main-drawer').should('be.visible');
});
it('Displays mobile menu on click', () => {
cy.get('.MuiDrawer-root').should('not.be.visible');
cy.get('#open-main-drawer').should('be.visible').click();
cy.get('.MuiDrawer-root').should('be.visible');
});
it('Displays a card list', () => {
cy.get('.MuiCard-root').should('have.length', 3);
});
});
context('720p resolution', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.visit('/c/maps');
});
it('Displays mobile menu button', () => {
cy.get('#open-desktop-drawer').should('be.visible');
});
it('Displays a table with maps', () => {
cy.get('.MuiTableBody-root').should('be.visible');
});
});

View File

@ -263,6 +263,33 @@
"info.title": {
"defaultMessage": "Información"
},
"label.add-button": {
"defaultMessage": "Agregar etiqueta"
},
"label.add-for": {
"defaultMessage": "Editando etiquetas para el mapa:"
},
"label.add-placeholder": {
"defaultMessage": "Título"
},
"label.change-color": {
"defaultMessage": "Cambiar el color"
},
"label.delete-description": {
"defaultMessage": "será borrado de todos los mapas a los que se encuentra asociado. Desea continuar?"
},
"label.delete-title": {
"defaultMessage": "Confirmación"
},
"label.description": {
"defaultMessage": "Usa las etiquetas para organizar tus mapas."
},
"label.maps-count": {
"defaultMessage": "{count} mapas"
},
"label.title": {
"defaultMessage": "Agregar etiqueta"
},
"language.change": {
"defaultMessage": "Cambiar idioma"
},

View File

@ -49,7 +49,6 @@
"@mui/icons-material": "^5.9.3",
"@mui/lab": "^5.0.0-alpha.98",
"@mui/material": "^5.10.11",
"@mui/styles": "^5.9.3",
"@reduxjs/toolkit": "^1.5.0",
"@wisemapping/editor": "^0.4.0",
"axios": "^0.27.2",

View File

@ -11,21 +11,16 @@ import { QueryClient, QueryClientProvider } from 'react-query';
import { theme } from './theme';
import AppI18n, { Locales } from './classes/app-i18n';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material/styles';
import { ThemeProvider as MuiThemeProvider, StyledEngineProvider } from '@mui/material/styles';
import ReactGA from 'react-ga4';
import AppConfig from './classes/app-config';
import withSessionExpirationHandling from './components/HOCs/withSessionExpirationHandling';
import RegistrationSuccessPage from './components/registration-success-page';
import { ThemeProvider } from '@emotion/react';
import RegistrationCallbackPage from './components/registration-callback';
const EditorPage = React.lazy(() => import('./components/editor-page'));
const MapsPage = React.lazy(() => import('./components/maps-page'));
declare module '@mui/styles/defaultTheme' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DefaultTheme extends Theme {}
}
// Google Analytics Initialization.
ReactGA.initialize([
{
@ -53,7 +48,6 @@ function Redirect({ to }) {
const App = (): ReactElement => {
const locale = AppI18n.getDefaultLocale();
const EnhacedEditorPage = withSessionExpirationHandling(EditorPage);
return locale.message ? (
<Provider store={store}>
@ -64,42 +58,67 @@ const App = (): ReactElement => {
messages={locale.message as Record<string, string>}
>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Routes>
<Route path="/" element={<Redirect to="/c/login" />} />
<Route path="/c/login" element={<LoginPage />} />
<Route path="/c/registration" element={<RegistationPage />} />
<Route path="/c/registration-google" element={<RegistrationCallbackPage />} />
<Route path="/c/registration-success" element={<RegistrationSuccessPage />} />
<Route path="/c/forgot-password" element={<ForgotPasswordPage />} />
<Route
path="/c/forgot-password-success"
element={<ForgotPasswordSuccessPage />}
/>
<Route
path="/c/maps/"
element={
<Suspense
fallback={
<div>
<FormattedMessage id="dialog.loading" defaultMessage="Loading ..." />
</div>
}
>
<MapsPage />
</Suspense>
}
/>
<Route
path="/c/maps/:id/edit"
element={<EnhacedEditorPage isTryMode={false} />}
/>
<Route path="/c/maps/:id/try" element={<EnhacedEditorPage isTryMode={true} />} />
</Routes>
</Router>
</ThemeProvider>
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Routes>
<Route path="/" element={<Redirect to="/c/login" />} />
<Route path="/c/login" element={<LoginPage />} />
<Route path="/c/registration" element={<RegistationPage />} />
<Route path="/c/registration-google" element={<RegistrationCallbackPage />} />
<Route path="/c/registration-success" element={<RegistrationSuccessPage />} />
<Route path="/c/forgot-password" element={<ForgotPasswordPage />} />
<Route
path="/c/forgot-password-success"
element={<ForgotPasswordSuccessPage />}
/>
<Route
path="/c/maps/"
element={
<Suspense
fallback={
<div>
<FormattedMessage id="dialog.loading" defaultMessage="Loading ..." />
</div>
}
>
<MapsPage />
</Suspense>
}
/>
<Route
path="/c/maps/:id/edit"
element={
<Suspense
fallback={
<div>
<FormattedMessage id="dialog.loading" defaultMessage="Loading ..." />
</div>
}
>
<EditorPage isTryMode={false} />
</Suspense>
}
/>
<Route
path="/c/maps/:id/try"
element={
<Suspense
fallback={
<div>
<FormattedMessage id="dialog.loading" defaultMessage="Loading ..." />
</div>
}
>
<EditorPage isTryMode={true} />
</Suspense>
}
/>
</Routes>
</Router>
</ThemeProvider>
</MuiThemeProvider>
</StyledEngineProvider>
</IntlProvider>
</QueryClientProvider>

View File

@ -386,7 +386,7 @@
"forgot.oauth.message": [
{
"type": 0,
"value": "You dont need password, please login using Google"
"value": "You dont need password, please login using Google."
}
],
"forgot.page-title": [
@ -631,6 +631,12 @@
"value": "Log into your account"
}
],
"login.division": [
{
"type": 0,
"value": "or"
}
],
"login.email": [
{
"type": 0,
@ -964,7 +970,7 @@
"registration.callback.waiting.title": [
{
"type": 0,
"value": "Finishing ..."
"value": "Finishing..."
}
],
"registration.desc": [

View File

@ -555,6 +555,64 @@
"value": "Información"
}
],
"label.add-button": [
{
"type": 0,
"value": "Agregar etiqueta"
}
],
"label.add-for": [
{
"type": 0,
"value": "Editando etiquetas para el mapa:"
}
],
"label.add-placeholder": [
{
"type": 0,
"value": "Título"
}
],
"label.change-color": [
{
"type": 0,
"value": "Cambiar el color"
}
],
"label.delete-description": [
{
"type": 0,
"value": "será borrado de todos los mapas a los que se encuentra asociado. Desea continuar?"
}
],
"label.delete-title": [
{
"type": 0,
"value": "Confirmación"
}
],
"label.description": [
{
"type": 0,
"value": "Usa las etiquetas para organizar tus mapas."
}
],
"label.maps-count": [
{
"type": 1,
"value": "count"
},
{
"type": 0,
"value": " mapas"
}
],
"label.title": [
{
"type": 0,
"value": "Agregar etiqueta"
}
],
"language.change": [
{
"type": 0,

View File

@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import React, { ComponentType } from 'react';
function withEmotionStyles<T>(css) {
return (Component: ComponentType<T>) => {
const WithEmotionStyles = (hocProps): React.ReactElement => {
return <Component {...hocProps} css={{ ...hocProps.paperCss, ...css }} />;
};
WithEmotionStyles.displayName = `withEmotionStyles(${getDisplayName(Component)})`;
return WithEmotionStyles;
};
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default withEmotionStyles;

View File

@ -1,42 +1,5 @@
import AppConfig from '../../classes/app-config';
import exampleMap from '../../classes/client/mock-client/example-map.wxml';
import {
PersistenceManager,
RESTPersistenceManager,
LocalStorageManager,
Mindmap,
MockPersistenceManager,
XMLSerializerFactory,
} from '@wisemapping/editor';
export const buildPersistenceManagerForEditor = (mode: string): PersistenceManager => {
let persistenceManager: PersistenceManager;
if (AppConfig.isRestClient()) {
if (mode === 'edition-owner' || mode === 'edition-editor') {
persistenceManager = new RESTPersistenceManager({
documentUrl: '/c/restful/maps/{id}/document',
revertUrl: '/c/restful/maps/{id}/history/latest',
lockUrl: '/c/restful/maps/{id}/lock',
});
} else {
persistenceManager = new LocalStorageManager(
`/c/restful/maps/{id}/${
globalThis.historyId ? `${globalThis.historyId}/` : ''
}document/xml${mode === 'showcase' ? '-pub' : ''}`,
true,
);
}
persistenceManager.addErrorHandler((error) => {
if (error.errorType === 'session-expired') {
// TODO: this line was in RestPersistenceClient, do something similar here
//client.sessionExpired();
}
});
} else {
persistenceManager = new MockPersistenceManager(exampleMap);
}
return persistenceManager;
};
import { LocalStorageManager, Mindmap, XMLSerializerFactory } from '@wisemapping/editor';
export const fetchMindmap = async (mapId: number): Promise<Mindmap> => {
let mindmap: Mindmap;

View File

@ -16,10 +16,14 @@
* limitations under the License.
*/
import React, { useEffect } from 'react';
import ActionDispatcher from '../maps-page/action-dispatcher';
import { ActionType } from '../maps-page/action-chooser';
import Editor from '@wisemapping/editor';
import { EditorRenderMode, PersistenceManager } from '@wisemapping/editor';
import {
EditorRenderMode,
PersistenceManager,
RESTPersistenceManager,
LocalStorageManager,
MockPersistenceManager,
} from '@wisemapping/editor';
import { IntlProvider } from 'react-intl';
import AppI18n, { Locales } from '../../classes/app-i18n';
import { useSelector } from 'react-redux';
@ -27,17 +31,66 @@ import { hotkeysEnabled } from '../../redux/editorSlice';
import ReactGA from 'react-ga4';
import { useFetchAccount, useFetchMapById } from '../../redux/clientSlice';
import EditorOptionsBuilder from './EditorOptionsBuilder';
import { buildPersistenceManagerForEditor } from './PersistenceManagerUtils';
import { useTheme } from '@mui/material/styles';
import AccountMenu from '../maps-page/account-menu';
import MapInfoImpl from '../../classes/editor-map-info';
import { MapInfo } from '@wisemapping/editor';
import { activeInstance } from '../../redux/clientSlice';
import Client from '../../classes/client';
import AppConfig from '../../classes/app-config';
import exampleMap from '../../classes/client/mock-client/example-map.wxml';
import withSessionExpirationHandling from '../HOCs/withSessionExpirationHandling';
const buildPersistenceManagerForEditor = (mode: string): PersistenceManager => {
let persistenceManager: PersistenceManager;
if (AppConfig.isRestClient()) {
if (mode === 'edition-owner' || mode === 'edition-editor') {
persistenceManager = new RESTPersistenceManager({
documentUrl: '/c/restful/maps/{id}/document',
revertUrl: '/c/restful/maps/{id}/history/latest',
lockUrl: '/c/restful/maps/{id}/lock',
});
} else {
persistenceManager = new LocalStorageManager(
`/c/restful/maps/{id}/${
globalThis.historyId ? `${globalThis.historyId}/` : ''
}document/xml${mode === 'showcase' ? '-pub' : ''}`,
true,
);
}
persistenceManager.addErrorHandler((error) => {
if (error.errorType === 'session-expired') {
// TODO: this line was in RestPersistenceClient, do something similar here
//client.sessionExpired();
}
});
} else {
persistenceManager = new MockPersistenceManager(exampleMap);
}
return persistenceManager;
};
export type EditorPropsType = {
isTryMode: boolean;
};
type ActionType =
| 'open'
| 'share'
| 'import'
| 'delete'
| 'info'
| 'create'
| 'duplicate'
| 'export'
| 'label'
| 'rename'
| 'print'
| 'info'
| 'publish'
| 'history'
| undefined;
const ActionDispatcher = React.lazy(() => import('../maps-page/action-dispatcher'));
const AccountMenu = React.lazy(() => import('../maps-page/account-menu'));
const EditorPage = ({ isTryMode }: EditorPropsType): React.ReactElement => {
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null);
@ -144,4 +197,4 @@ const EditorPage = ({ isTryMode }: EditorPropsType): React.ReactElement => {
);
};
export default EditorPage;
export default withSessionExpirationHandling(EditorPage);

View File

@ -69,7 +69,7 @@ const ForgotPassword = () => {
}
return (
<FormContainer>
<FormContainer maxWidth="xs">
<Typography variant="h4" component="h1">
<FormattedMessage id="forgot.title" defaultMessage="Reset your password" />
</Typography>

View File

@ -1,11 +1,9 @@
import withStyles from '@mui/styles/withStyles';
import withEmotionStyles from '../../HOCs/withEmotionStyles';
import Alert from '@mui/material/Alert';
export const StyledAlert = withStyles({
root: {
padding: '10px 15px',
margin: '5px 0px ',
},
export const StyledAlert = withEmotionStyles({
padding: '10px 15px',
margin: '5px 0px ',
})(Alert);
export default StyledAlert;

View File

@ -1,12 +1,10 @@
import withEmotionStyles from '../../HOCs/withEmotionStyles';
import Container from '@mui/material/Container';
import withStyles from '@mui/styles/withStyles';
const FormContainer = withStyles({
root: {
padding: '20px 10px 0px 20px',
maxWidth: '380px',
textAlign: 'center',
},
const FormContainer = withEmotionStyles({
padding: '20px 10px 0px 20px',
maxWidth: '380px !important',
textAlign: 'center',
})(Container);
export default FormContainer;

View File

@ -52,7 +52,7 @@ const ActionChooser = (props: ActionProps): React.ReactElement => {
};
};
const role = mapId ? useFetchMapById(mapId)?.map?.role : undefined;
const role = mapId !== undefined ? useFetchMapById(mapId)?.map?.role : undefined;
return (
<Menu
anchorEl={anchor}

View File

@ -1,7 +1,7 @@
import MenuItem from '@mui/material/MenuItem';
import withStyles from '@mui/styles/withStyles';
import withEmotionStyles from '../../HOCs/withEmotionStyles';
export const StyledMenuItem = withStyles({
export const StyledMenuItem = withEmotionStyles({
root: {
width: '300px',
},

View File

@ -5,9 +5,9 @@ import { StyledDialog, StyledDialogActions, StyledDialogContent, StyledDialogTit
import GlobalError from '../../../form/global-error';
import DialogContentText from '@mui/material/DialogContentText';
import Button from '@mui/material/Button';
import { PaperProps } from '@mui/material/Paper';
import { useDispatch } from 'react-redux';
import { disableHotkeys, enableHotkeys } from '../../../../redux/editorSlice';
import { CSSObject } from '@emotion/react';
export type DialogProps = {
onClose: () => void;
@ -21,7 +21,7 @@ export type DialogProps = {
submitButton?: string;
actionUrl?: string;
maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false;
PaperProps?: Partial<PaperProps>;
paperCss?: CSSObject;
};
const BaseDialog = (props: DialogProps): React.ReactElement => {
@ -32,7 +32,7 @@ const BaseDialog = (props: DialogProps): React.ReactElement => {
dispatch(enableHotkeys());
};
}, []);
const { onClose, onSubmit, maxWidth = 'sm', PaperProps } = props;
const { onClose, onSubmit, maxWidth = 'sm', paperCss } = props;
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -50,7 +50,7 @@ const BaseDialog = (props: DialogProps): React.ReactElement => {
open={true}
onClose={onClose}
maxWidth={maxWidth}
PaperProps={PaperProps}
paperCss={{ '& .MuiPaper-root.MuiDialog-paper': paperCss }}
fullWidth={true}
>
<form autoComplete="off" onSubmit={handleOnSubmit}>

View File

@ -2,28 +2,20 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import withStyles from '@mui/styles/withStyles';
import withEmotionStyles from '../../../HOCs/withEmotionStyles';
export const StyledDialogContent = withStyles({
root: {
padding: '0px 39px',
},
export const StyledDialogContent = withEmotionStyles({
padding: '0px 39px',
})(DialogContent);
export const StyledDialogTitle = withStyles({
root: {
padding: '39px 39px 10px 39px',
},
export const StyledDialogTitle = withEmotionStyles({
padding: '39px 39px 10px 39px',
})(DialogTitle);
export const StyledDialogActions = withStyles({
root: {
padding: '39px 39px 39px 39px',
},
export const StyledDialogActions = withEmotionStyles({
padding: '39px 39px 39px 39px',
})(DialogActions);
export const StyledDialog = withStyles({
root: {
borderRadius: '9px',
},
export const StyledDialog = withEmotionStyles({
borderRadius: '9px',
})(Dialog);

View File

@ -185,7 +185,7 @@ const ExportDialog = ({
<RadioGroup name="export" value={exportGroup} onChange={handleOnGroupChange}>
<FormControl>
<FormControlLabel
className={classes.label}
css={classes.label}
value="image"
disabled={!enableImgExport}
control={<Radio color="primary" />}
@ -203,20 +203,20 @@ const ExportDialog = ({
onChange={handleOnExportFormatChange}
variant="outlined"
value={exportFormat}
className={classes.select}
css={classes.select}
>
<MenuItem value="svg" className={classes.menu}>
<MenuItem value="svg" css={classes.menu}>
Scalable Vector Graphics (SVG)
</MenuItem>
<MenuItem value="png" className={classes.menu}>
<MenuItem value="png" css={classes.menu}>
Portable Network Graphics (PNG)
</MenuItem>
<MenuItem value="jpg" className={classes.menu}>
<MenuItem value="jpg" css={classes.menu}>
JPEG Image (JPEG)
</MenuItem>
</Select>
<FormControlLabel
className={classes.select}
css={classes.select}
control={<Checkbox checked={zoomToFit} onChange={handleOnZoomToFit} />}
label={intl.formatMessage({
id: 'export.img-center',
@ -229,7 +229,7 @@ const ExportDialog = ({
<FormControl>
<FormControlLabel
className={classes.label}
css={classes.label}
value="document"
control={<Radio color="primary" />}
label={intl.formatMessage({
@ -244,12 +244,12 @@ const ExportDialog = ({
onChange={handleOnExportFormatChange}
variant="outlined"
value={exportFormat}
className={classes.select}
css={classes.select}
>
<MenuItem className={classes.select} value="txt">
<MenuItem css={classes.select} value="txt">
Plain Text File (TXT)
</MenuItem>
<MenuItem className={classes.select} value="md">
<MenuItem css={classes.select} value="md">
Markdown (MD)
</MenuItem>
{/* <MenuItem className={classes.select} value="xls">
@ -261,7 +261,7 @@ const ExportDialog = ({
<FormControl>
<FormControlLabel
className={classes.label}
css={classes.label}
value="mindmap-tool"
control={<Radio color="primary" />}
label={intl.formatMessage({
@ -275,13 +275,13 @@ const ExportDialog = ({
<Select
onChange={handleOnExportFormatChange}
variant="outlined"
className={classes.select}
css={classes.select}
value={exportFormat}
>
<MenuItem className={classes.select} value="wxml">
<MenuItem css={classes.select} value="wxml">
WiseMapping (WXML)
</MenuItem>
<MenuItem className={classes.select} value="mm">
<MenuItem css={classes.select} value="mm">
Freemind 1.0.1 (MM)
</MenuItem>
{/* <MenuItem className={classes.select} value="mmap">

View File

@ -1,8 +1,8 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () =>
useClasses({
select: {
height: '40px',
borderRadius: '9px',
@ -16,5 +16,4 @@ export const useStyles = makeStyles(() =>
label: {
margin: '5px 0px',
},
}),
);
});

View File

@ -50,9 +50,11 @@ const ActionDispatcher = ({
switch (action) {
case 'open':
window.location.href = `/c/maps/${mapsId}/edit`;
handleOnClose(true);
break;
case 'print':
window.open(`/c/maps/${mapsId}/print`, 'print');
handleOnClose(true);
break;
}

View File

@ -42,7 +42,7 @@ const InfoDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement =
>
<Paper style={{ maxHeight: 200, overflowY: 'scroll' }} variant="outlined" elevation={0}>
<Card variant="outlined">
<List dense={true}>
<List dense={true} css={classes.list}>
<ListItem>
<Typography variant="body1" style={{ fontWeight: 'bold' }}>
<FormattedMessage id="info.basic-info" defaultMessage="Basic Info" />
@ -50,42 +50,42 @@ const InfoDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement =
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.name" defaultMessage="Name" />:
</Typography>
<Typography variant="body2">{map?.title}</Typography>
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.description" defaultMessage="Description" />:
</Typography>
<Typography variant="body2">{map?.description}</Typography>
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.creator" defaultMessage="Creator" />:
</Typography>
<Typography variant="body2">{map?.createdBy}</Typography>
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.creation-time" defaultMessage="Creation Date" />:
</Typography>
<Typography variant="body2">{dayjs(map?.creationTime).format('LLL')}</Typography>
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.modified-tny" defaultMessage="Last Modified By" />:
</Typography>
<Typography variant="body2">{map?.lastModificationBy}</Typography>
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.modified-time" defaultMessage="Last Modified Date" />:
</Typography>
<Typography variant="body2">
@ -94,7 +94,7 @@ const InfoDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement =
</ListItem>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.starred" defaultMessage="Starred" />:
</Typography>
<Typography variant="body2">{Boolean(map?.starred).toString()}</Typography>
@ -111,7 +111,7 @@ const InfoDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement =
</ListItem>
</List>
<ListItem>
<Typography variant="caption" color="textPrimary" className={classes.textDesc}>
<Typography variant="caption" color="textPrimary" css={classes.textDesc}>
<FormattedMessage id="info.public-visibility" defaultMessage="Publicly Visible" />:
</Typography>
<Typography variant="body2">{Boolean(map?.isPublic).toString()}</Typography>

View File

@ -1,8 +1,9 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useTheme } from '@mui/material/styles';
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () =>
useClasses({
textarea: {
width: '100%',
padding: '15px 15px',
@ -11,5 +12,14 @@ export const useStyles = makeStyles(() =>
textDesc: {
width: '150px',
},
}),
);
list: {
'& li': {
[useTheme().breakpoints.down('sm')]: {
display: 'block',
},
'& p': {
marginLeft: 5,
},
},
},
});

View File

@ -11,6 +11,7 @@ import Client, { ErrorInfo, Label, MapInfo } from '../../../../classes/client';
import { LabelSelector } from '../../maps-list/label-selector';
import { activeInstance } from '../../../../redux/clientSlice';
import { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list';
import { Interpolation, Theme } from '@emotion/react';
const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => {
const intl = useIntl();
@ -62,11 +63,11 @@ const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement
id: 'label.description',
defaultMessage: 'Use labels to organize your maps.',
})}
PaperProps={{ classes: { root: classes.paper } }}
paperCss={classes.paper}
error={error}
>
<>
<Typography variant="body2" marginTop="10px">
<Typography variant="body2" marginTop="10px" css={classes.title as Interpolation<Theme>}>
<FormattedMessage id="label.add-for" defaultMessage="Editing labels for " />
{maps.length > 1 ? (
<FormattedMessage

View File

@ -1,10 +1,13 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () =>
useClasses({
paper: {
maxWidth: '420px',
},
}),
);
title: {
maxWidth: '100%',
wordBreak: 'break-all',
},
});

View File

@ -122,7 +122,7 @@ const PublishDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElemen
/>
</Typography>
<TextareaAutosize
className={classes.textarea}
css={classes.textarea}
readOnly={true}
spellCheck={false}
maxRows={6}
@ -137,7 +137,7 @@ const PublishDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElemen
/>
</Typography>
<TextareaAutosize
className={classes.textarea}
css={classes.textarea}
readOnly={true}
spellCheck={false}
maxRows={1}

View File

@ -1,12 +1,11 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () =>
useClasses({
textarea: {
width: '100%',
padding: '15px 15px',
marging: '0px 10px',
},
}),
);
});

View File

@ -24,6 +24,7 @@ import Typography from '@mui/material/Typography';
import { useStyles } from './style';
import RoleIcon from '../../role-icon';
import Tooltip from '@mui/material/Tooltip';
import { Interpolation, Theme } from '@emotion/react';
type ShareModel = {
emails: string;
@ -138,15 +139,14 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
defaultMessage:
'Invite people to collaborate with you in the creation of your mindmap. They will be notified by email. ',
})}
PaperProps={{ classes: { root: classes.paper } }}
paperCss={classes.paper}
error={error}
>
<div className={classes.actionContainer}>
<div css={classes.actionContainer as Interpolation<Theme>}>
<TextField
id="emails"
name="emails"
required={true}
style={{ width: '300px' }}
size="small"
type="email"
variant="outlined"
@ -154,6 +154,7 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
label="Emails"
onChange={handleOnChange}
value={model.emails}
css={[classes.fullWidthInMobile, classes.email]}
/>
<Select
@ -161,7 +162,7 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
onChange={handleOnChange}
value={model.role}
name="role"
style={{ margin: '0px 10px' }}
css={[classes.fullWidthInMobile, classes.role]}
>
<MenuItem value="editor">
<FormattedMessage id="share.can-edit" defaultMessage="Can edit" />
@ -202,7 +203,7 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
multiline
rows={3}
maxRows={3}
className={classes.textArea}
css={classes.textArea}
variant="filled"
name="message"
onChange={handleOnChange}
@ -216,13 +217,17 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement
</div>
{!isLoading && (
<Paper elevation={1} className={classes.listPaper} variant="outlined">
<Paper elevation={1} css={classes.listPaper as Interpolation<Theme>} variant="outlined">
<List>
{permissions &&
permissions.map((permission) => {
return (
<ListItem key={permission.email} role={undefined} dense button>
<ListItemText id={permission.email} primary={formatName(permission)} />
<ListItemText
css={classes.listItemText as Interpolation<Theme>}
id={permission.email}
primary={formatName(permission)}
/>
<RoleIcon role={permission.role} />
<ListItemSecondaryAction>

View File

@ -1,18 +1,37 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useTheme } from '@mui/material/styles';
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () =>
useClasses({
actionContainer: {
padding: '10px 0px',
border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '8px 8px 0px 0px',
textAlign: 'center',
},
fullWidthInMobile: {
[useTheme().breakpoints.down('sm')]: {
minWidth: '99%',
marginBottom: 5,
},
},
email: {
[useTheme().breakpoints.up('sm')]: {
width: '300px',
},
},
role: {
[useTheme().breakpoints.up('sm')]: {
margin: '0px 10px',
},
},
textArea: {
width: '730px',
margin: '5px 0px',
padding: '10px',
[useTheme().breakpoints.up('sm')]: {
width: '730px',
margin: '5px 0px',
padding: '10px',
},
},
listPaper: {
maxHeight: 200,
@ -21,6 +40,16 @@ export const useStyles = makeStyles(() =>
paper: {
width: '850px',
minWidth: '850px',
[useTheme().breakpoints.down('sm')]: {
minWidth: '100%',
},
},
}),
);
listItemText: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
'& span': {
display: 'inline',
},
},
});

View File

@ -1,5 +1,4 @@
import React, { ErrorInfo, ReactElement, useEffect } from 'react';
import clsx from 'clsx';
import Drawer from '@mui/material/Drawer';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
@ -20,6 +19,9 @@ import LanguageMenu from './language-menu';
import AppI18n, { Locales } from '../../classes/app-i18n';
import ListItemIcon from '@mui/material/ListItemIcon';
import MenuIcon from '@mui/icons-material/Menu';
import ArrowRight from '@mui/icons-material/NavigateNext';
import ArrowLeft from '@mui/icons-material/NavigateBefore';
import AddCircleTwoTone from '@mui/icons-material/AddCircleTwoTone';
import CloudUploadTwoTone from '@mui/icons-material/CloudUploadTwoTone';
@ -41,7 +43,8 @@ import logoIcon from './logo-small.svg';
import poweredByIcon from './pwrdby-white.svg';
import LabelDeleteConfirm from './maps-list/label-delete-confirm';
import ReactGA from 'react-ga4';
import { withStyles } from '@mui/styles';
import { CSSObject, Interpolation, Theme } from '@emotion/react';
import withEmotionStyles from '../HOCs/withEmotionStyles';
export type Filter = GenericFilter | LabelFilter;
@ -61,12 +64,22 @@ interface ToolbarButtonInfo {
}
const MapsPage = (): ReactElement => {
const classes = useStyles();
const [filter, setFilter] = React.useState<Filter>({ type: 'all' });
const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient();
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null);
const [mobileOpen, setMobileOpen] = React.useState(false);
const [desktopOpen, setDesktopOpen] = React.useState(true);
const classes = useStyles(desktopOpen);
const handleMobileDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleDesktopDrawerToggle = () => {
setDesktopOpen(!desktopOpen);
};
// Reload based on user preference ...
const userLocale = AppI18n.getUserLocale();
@ -85,6 +98,7 @@ const MapsPage = (): ReactElement => {
id: 'maps.page-title',
defaultMessage: 'My Maps | WiseMapping',
});
window.scrollTo(0, 0);
ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: 'Maps List' });
}, []);
@ -101,6 +115,7 @@ const MapsPage = (): ReactElement => {
const handleMenuClick = (filter: Filter) => {
queryClient.invalidateQueries('maps');
setFilter(filter);
mobileOpen && setMobileOpen(false);
};
const handleLabelDelete = (id: number) => {
@ -148,22 +163,73 @@ const MapsPage = (): ReactElement => {
}),
);
const drawerItemsList = (
<>
<div style={{ padding: '20px 0 20px 16px' }} key="logo">
<img src={logoIcon} alt="logo" />
</div>
<List component="nav">
{filterButtons.map((buttonInfo) => {
return (
<StyleListItem
icon={buttonInfo.icon}
label={buttonInfo.label}
filter={buttonInfo.filter}
active={filter}
onClick={handleMenuClick}
onDelete={setLabelToDelete}
key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
/>
);
})}
</List>
<div
className="poweredByIcon"
style={{ position: 'absolute', bottom: '10px', left: '20px' }}
key="power-by"
>
<Link href="http://www.wisemapping.org/">
<img src={poweredByIcon} alt="Powered By WiseMapping" />
</Link>
</div>
</>
);
const container = document !== undefined ? () => document.body : undefined;
return (
<IntlProvider
locale={userLocale.code}
defaultLocale={Locales.EN.code}
messages={userLocale.message}
>
<div className={classes.root}>
<div css={classes.root}>
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
css={[classes.appBar, open && classes.appBarShift]}
variant="outlined"
elevation={0}
>
<Toolbar>
<IconButton
aria-label="open drawer"
edge="start"
onClick={handleMobileDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
id="open-main-drawer"
>
<MenuIcon />
</IconButton>
<IconButton
aria-label="open drawer"
edge="start"
onClick={handleDesktopDrawerToggle}
sx={{ p: 0, mr: 2, display: { xs: 'none', sm: 'inherit' } }}
id="open-desktop-drawer"
>
{!desktopOpen && <ArrowRight />}
{desktopOpen && <ArrowLeft />}
</IconButton>
<Tooltip
arrow={true}
title={intl.formatMessage({
@ -179,10 +245,12 @@ const MapsPage = (): ReactElement => {
type="button"
disableElevation={true}
startIcon={<AddCircleTwoTone />}
className={classes.newMapButton}
css={classes.newMapButton}
onClick={() => setActiveDialog('create')}
>
<FormattedMessage id="action.new" defaultMessage="New map" />
<span className="message">
<FormattedMessage id="action.new" defaultMessage="New map" />
</span>
</Button>
</Tooltip>
@ -200,10 +268,12 @@ const MapsPage = (): ReactElement => {
type="button"
disableElevation={true}
startIcon={<CloudUploadTwoTone />}
className={classes.importButton}
css={classes.importButton}
onClick={() => setActiveDialog('import')}
>
<FormattedMessage id="action.import" defaultMessage="Import" />
<span className="message">
<FormattedMessage id="action.import" defaultMessage="Import" />
</span>
</Button>
</Tooltip>
<ActionDispatcher
@ -213,7 +283,7 @@ const MapsPage = (): ReactElement => {
fromEditor
/>
<div className={classes.rightButtonGroup}>
<div css={classes.rightButtonGroup as Interpolation<Theme>}>
<LanguageMenu />
<HelpMenu />
<AccountMenu />
@ -221,44 +291,29 @@ const MapsPage = (): ReactElement => {
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
}),
container={container}
variant={'temporary'}
open={mobileOpen}
onClose={handleMobileDrawerToggle}
ModalProps={{
keepMounted: true,
}}
css={[classes.mobileDrawer, { '& .MuiPaper-root': classes.drawerOpen }]}
>
<div style={{ padding: '20px 0 20px 15px' }} key="logo">
<img src={logoIcon} alt="logo" />
</div>
<List component="nav">
{filterButtons.map((buttonInfo) => {
return (
<StyleListItem
icon={buttonInfo.icon}
label={buttonInfo.label}
filter={buttonInfo.filter}
active={filter}
onClick={handleMenuClick}
onDelete={setLabelToDelete}
key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
/>
);
})}
</List>
<div style={{ position: 'absolute', bottom: '10px', left: '20px' }} key="power-by">
<Link href="http://www.wisemapping.org/">
<img src={poweredByIcon} alt="Powered By WiseMapping" />
</Link>
</div>
{drawerItemsList}
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<Drawer
variant="permanent"
css={[
classes.drawer as CSSObject,
classes.drawerOpen,
{ '& .MuiPaper-root': classes.drawerOpen },
]}
>
{drawerItemsList}
</Drawer>
<main css={classes.content}>
<div css={classes.toolbar} />
<MapsList filter={filter} />
</main>
</div>
@ -286,24 +341,21 @@ interface ListItemProps {
}
// https://stackoverflow.com/questions/61486061/how-to-set-selected-and-hover-color-of-listitem-in-mui
const CustomListItem = withStyles({
root: {
'&$selected': {
backgroundColor: 'rgb(210, 140, 5)',
const CustomListItem = withEmotionStyles({
'&.Mui-selected': {
backgroundColor: 'rgb(210, 140, 5)',
color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
},
},
'&.Mui-selected:hover': {
backgroundColor: 'rgb(210, 140, 5)',
color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
},
},
'&$selected:hover': {
backgroundColor: 'rgb(210, 140, 5)',
color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
},
},
},
selected: {},
})(ListItemButton);
const StyleListItem = (props: ListItemProps) => {
@ -336,7 +388,9 @@ const StyleListItem = (props: ListItemProps) => {
return (
<CustomListItem selected={isSelected} onClick={(e) => handleOnClick(e, filter)}>
<ListItemIcon>{icon}</ListItemIcon>
<Tooltip title={label} disableInteractive>
<ListItemIcon>{icon}</ListItemIcon>
</Tooltip>
<ListItemText style={{ color: 'white' }} primary={label} />
{filter.type == 'label' && (
<ListItemSecondaryAction>

View File

@ -16,6 +16,8 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Divider from '@mui/material/Divider';
import { useTheme } from '@mui/material/styles';
import { mobileAppbarButton } from '../style';
const LanguageMenu = (): React.ReactElement => {
const queryClient = useQueryClient();
@ -25,6 +27,8 @@ const LanguageMenu = (): React.ReactElement => {
const open = Boolean(anchorEl);
const intl = useIntl();
const theme = useTheme();
const smMediaQuery = theme.breakpoints.down('sm');
// Todo: For some reasons, in some situations locale is null. More research needed.
const mutation = useMutation(
@ -68,11 +72,14 @@ const LanguageMenu = (): React.ReactElement => {
variant="outlined"
disableElevation={true}
color="primary"
css={{
[smMediaQuery]: mobileAppbarButton,
}}
style={{ borderColor: 'gray', color: 'gray' }}
onClick={handleMenu}
startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />}
>
{userLocale.label}
<span className="message">{userLocale.label}</span>
</Button>
</Tooltip>
<Menu

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -29,6 +29,8 @@ import InputBase from '@mui/material/InputBase';
import Link from '@mui/material/Link';
import DeleteOutlined from '@mui/icons-material/DeleteOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded';
import SearchIcon from '@mui/icons-material/Search';
@ -38,6 +40,12 @@ import { LabelsCell } from './labels-cell';
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import AppI18n from '../../../classes/app-i18n';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import { CSSObject, Interpolation, Theme } from '@emotion/react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader';
dayjs.extend(LocalizedFormat);
dayjs.extend(relativeTime);
@ -134,7 +142,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
padding="checkbox"
key="select"
style={{ width: '20px' }}
className={classes.headerCell}
css={classes.headerCell}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
@ -145,7 +153,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
/>
</TableCell>
<TableCell padding="checkbox" key="starred" className={classes.headerCell}></TableCell>
<TableCell padding="checkbox" key="starred" css={classes.headerCell}></TableCell>
{headCells.map((headCell) => {
return (
@ -153,7 +161,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false}
style={headCell.style}
className={classes.headerCell}
css={classes.headerCell}
>
<TableSortLabel
active={orderBy === headCell.id}
@ -163,7 +171,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
{headCell.label}
{orderBy === headCell.id && (
<span className={classes.visuallyHidden}>
<span css={classes.visuallyHidden as Interpolation<Theme>}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</span>
)}
@ -172,7 +180,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
);
})}
<TableCell padding="checkbox" key="action" className={classes.headerCell}></TableCell>
<TableCell padding="checkbox" key="action" css={classes.headerCell}></TableCell>
</TableRow>
</TableHead>
);
@ -334,7 +342,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
mapId: mapId,
el: event.currentTarget,
});
event.stopPropagation();
event.preventDefault();
};
};
@ -364,7 +372,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
);
const handleStarred = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: number) => {
event.stopPropagation();
event.preventDefault();
starredMultation.mutate(id);
};
@ -423,16 +431,16 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
const isSelected = (id: number) => selected.indexOf(id) !== -1;
return (
<div className={classes.root}>
<div css={classes.root}>
<ActionChooser
anchor={activeRowAction?.el}
onClose={handleActionMenuClose}
mapId={activeRowAction?.mapId}
/>
<Paper className={classes.paper} elevation={0}>
<Toolbar className={classes.toolbar} variant="dense">
<div className={classes.toolbarActions}>
<Paper css={classes.paper} elevation={0}>
<Toolbar css={classes.toolbar} variant="dense">
<div css={classes.toolbarActions}>
{selected.length > 0 && (
<Tooltip
arrow={true}
@ -479,9 +487,24 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
)}
</div>
<div className={classes.toolbarListActions}>
<div>
<div css={classes.search as Interpolation<Theme>}>
<div css={classes.searchIcon as Interpolation<Theme>}>
<SearchIcon />
</div>
<InputBase
placeholder={intl.formatMessage({
id: 'maps.search-action',
defaultMessage: 'Search ...',
})}
css={[classes.searchInputRoot, classes.searchInputInput]}
inputProps={{ 'aria-label': 'search' }}
onChange={handleOnSearchChange}
// startAdornment={<SearchIcon />}
/>
</div>
<TablePagination
style={{ float: 'right', border: '0', paddingBottom: '5px' }}
css={classes.tablePagination as Interpolation<Theme>}
count={mapsInfo.length}
rowsPerPageOptions={[]}
rowsPerPage={rowsPerPage}
@ -490,29 +513,108 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
onRowsPerPageChange={handleChangeRowsPerPage}
component="div"
/>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder={intl.formatMessage({
id: 'maps.search-action',
defaultMessage: 'Search ...',
})}
classes={{
root: classes.searchInputRoot,
input: classes.searchInputInput,
}}
inputProps={{ 'aria-label': 'search' }}
onChange={handleOnSearchChange}
/>
</div>
</div>
</Toolbar>
<TableContainer>
<Table className={classes.table} size="small" stickyHeader>
<Box css={classes.cards}>
{isLoading ? (
<Card>
<CardContent>Loading ...</CardContent>
</Card>
) : mapsInfo.length == 0 ? (
<Card>
<CardContent>
<FormattedMessage
id="maps.empty-result"
defaultMessage="No matching mindmap found with the current filter criteria."
/>
</CardContent>
</Card>
) : (
stableSort(mapsInfo, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row: MapInfo) => {
return (
<Card key={row.id} css={{ maxWidth: '94vw', margin: '3vw' }}>
<Link
href={`/c/maps/${row.id}/edit`}
underline="none"
onClick={(e) => e.stopPropagation()}
>
<CardHeader
css={classes.cardHeader}
avatar={
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'maps.tooltip-starred',
defaultMessage: 'Starred',
})}
>
<div className="hola" onClick={(e) => e.stopPropagation()}>
<IconButton size="small" onClick={(e) => handleStarred(e, row.id)}>
<StarRateRoundedIcon
color="action"
style={{
color: row.starred ? 'yellow' : 'gray',
}}
/>
</IconButton>
</div>
</Tooltip>
}
action={
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'map.more-actions',
defaultMessage: 'More Actions',
})}
>
<IconButton aria-label="settings" onClick={handleActionClick(row.id)}>
<MoreVertIcon color="action" />
</IconButton>
</Tooltip>
}
title={
<Typography sx={{ fontSize: 'large' }} noWrap color="text.secondary">
{row.title}
</Typography>
}
subheader={
<Typography variant="subtitle2">
{intl.formatMessage({
id: 'map.last-update',
defaultMessage: 'Last Update',
})}
<span>: </span>
<Tooltip
arrow={true}
title={intl.formatMessage(
{
id: 'maps.modified-by-desc',
defaultMessage: 'Modified by {by} on {on}',
},
{
by: row.lastModificationBy,
on: dayjs(row.lastModificationTime).format('lll'),
},
)}
placement="bottom-start"
>
<span>{dayjs(row.lastModificationTime).fromNow()}</span>
</Tooltip>
</Typography>
}
/>
</Link>
</Card>
);
})
)}
</Box>
<Table css={classes.table} size="small" stickyHeader>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
@ -555,7 +657,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
selected={isItemSelected}
style={{ border: '0' }}
>
<TableCell padding="checkbox" className={classes.bodyCell}>
<TableCell padding="checkbox" css={classes.bodyCell}>
<Checkbox
checked={isItemSelected}
inputProps={{
@ -565,7 +667,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
/>
</TableCell>
<TableCell padding="checkbox" className={classes.bodyCell}>
<TableCell padding="checkbox" css={classes.bodyCell}>
<Tooltip
arrow={true}
title={intl.formatMessage({
@ -584,7 +686,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
<TableCell css={classes.bodyCell}>
<Tooltip
arrow={true}
title={intl.formatMessage({
@ -604,7 +706,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip>
</TableCell>
<TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}>
<TableCell css={[classes.bodyCell, classes.labelsCell as CSSObject]}>
<LabelsCell
labels={row.labels}
onDelete={(lbl) => {
@ -613,9 +715,9 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
/>
</TableCell>
<TableCell className={classes.bodyCell}>{row.createdBy}</TableCell>
<TableCell css={classes.bodyCell}>{row.createdBy}</TableCell>
<TableCell className={classes.bodyCell}>
<TableCell css={classes.bodyCell}>
<Tooltip
arrow={true}
title={intl.formatMessage(
@ -634,7 +736,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
<TableCell css={classes.bodyCell}>
<Tooltip
arrow={true}
title={intl.formatMessage({

View File

@ -1,28 +1,37 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { alpha, useTheme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
import useClasses from '../../../theme/useStyles';
export const useStyles = makeStyles(() =>
createStyles({
export const useStyles = () => {
const theme = useTheme();
const smMediaQuery = theme.breakpoints.down('sm');
return useClasses({
root: {
width: '100%',
},
paper: {
width: '100%',
marginBottom: useTheme().spacing(2),
marginBottom: theme.spacing(2),
},
cards: {
display: 'none',
[smMediaQuery]: {
display: 'block',
},
},
table: {
[smMediaQuery]: {
display: 'none',
},
minWidth: 750,
'& tr:nth-child(even)': {
'& tr:nth-of-type(2n)': {
background: 'white',
},
'& tr:nth-child(odd)': {
'& tr:nth-of-type(2n+1)': {
background: 'rgba(221, 221, 221, 0.35)',
},
// '&:hover tr': {
// backgroundColor: 'rgba(150, 150, 150, 0.7)',
// }
},
headerCell: {
background: 'white',
@ -61,22 +70,31 @@ export const useStyles = makeStyles(() =>
flexGrow: 1,
paddingLeft: '23px;',
},
toolbarListActions: {
flexGrow: 1,
},
search: {
borderRadius: 9,
backgroundColor: alpha(useTheme().palette.common.white, 0.15),
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(useTheme().palette.common.white, 0.25),
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
margin: '10px 0px',
width: '100%',
[useTheme().breakpoints.up('sm')]: {
marginLeft: useTheme().spacing(1),
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
float: 'left',
[smMediaQuery]: {
width: '50%',
},
},
tablePagination: {
float: 'right',
border: '0',
paddingBottom: '5px',
[smMediaQuery]: {
width: '50%',
overflow: 'hidden',
},
},
searchIcon: {
padding: '6px 0 0 5px',
@ -93,19 +111,22 @@ export const useStyles = makeStyles(() =>
float: 'right',
},
searchInputInput: {
// padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
border: '1px solid #ffa800',
borderRadius: 4,
paddingLeft: `calc(1em + ${useTheme().spacing(4)})`,
transition: useTheme().transitions.create('width'),
width: '100%',
[useTheme().breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
'& .MuiInputBase-input': {
border: '1px solid #ffa800',
borderRadius: 4,
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
},
}),
);
cardHeader: {
padding: '4px',
},
});
};

View File

@ -1,10 +1,45 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { CSSObject } from '@emotion/react';
import { Theme, useTheme } from '@mui/material/styles';
import useClasses from '../../theme/useStyles';
const drawerWidth = 300;
const openedMixin = (theme: Theme, drawerWidth): CSSObject => ({
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
});
export const useStyles = makeStyles(() =>
createStyles({
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: `calc(${theme.spacing(7)} + 1px)`,
[theme.breakpoints.up('sm')]: {
width: `calc(${theme.spacing(8)} + 1px)`,
},
});
export const mobileAppbarButton = {
padding: 0,
minWidth: 'unset',
'& .message': {
display: 'none',
},
'& .MuiButton-startIcon': {
margin: 0,
padding: 10,
},
};
export function useStyles(drawerOpen) {
const drawerWidth = drawerOpen ? 300 : 66;
const theme = useTheme();
const smMediaQuery = theme.breakpoints.down('sm');
return useClasses({
root: {
display: 'flex',
},
@ -14,29 +49,70 @@ export const useStyles = makeStyles(() =>
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
[smMediaQuery]: {
width: '100%',
},
},
newMapButton: {
marginRight: 10,
minWidth: '130px',
[smMediaQuery]: mobileAppbarButton,
},
importButton: {
marginRight: 10,
minWidth: '130px',
[smMediaQuery]: mobileAppbarButton,
},
rightButtonGroup: {
marginRight: 10,
flexGrow: 10,
textAlign: 'right',
minWidth: '280px',
[smMediaQuery]: {
minWidth: 'unset',
marginRight: 0,
},
},
drawer: {
width: drawerWidth,
flexShrink: 0,
[smMediaQuery]: {
display: 'none',
},
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(drawerOpen && {
...openedMixin(theme, drawerWidth),
'& .MuiDrawer-paper': openedMixin(theme, drawerWidth),
}),
...(!drawerOpen && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme),
}),
'& .MuiListItemText-root': {
display: 'block',
...(!drawerOpen && {
color: 'transparent',
'& span': { color: 'transparent' },
}),
},
'& .MuiListItemSecondaryAction-root, & .poweredByIcon': {
display: 'block',
...(!drawerOpen && { display: 'none' }),
},
},
mobileDrawer: {
display: 'none',
[smMediaQuery]: {
display: 'block',
},
},
drawerOpen: {
background: '#ffa800',
width: drawerWidth,
[smMediaQuery]: {
width: 300,
},
},
toolbar: {
display: 'flex',
@ -47,5 +123,5 @@ export const useStyles = makeStyles(() =>
flexGrow: 1,
padding: '24px 0px',
},
}),
);
});
}

View File

@ -0,0 +1,20 @@
import { css } from '@emotion/react';
import { useTheme } from '@emotion/react';
const getStyle = (value) => {
return css(value);
};
function useClasses<T>(stylesElement: T): T {
const theme = useTheme();
const rawClasses = typeof stylesElement === 'function' ? stylesElement(theme) : stylesElement;
const prepared = {};
Object.entries(rawClasses).forEach(([key, value]) => {
prepared[key] = getStyle(value);
});
return prepared as T;
}
export default useClasses;