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(); topic.closeEditors();
} }
static useCreate(): [boolean, Element | undefined, DefaultWidgetManager] { static useCreate(): [boolean, (boolean) => void, Element | undefined, DefaultWidgetManager] {
const [popoverOpen, setPopoverOpen] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverTarget, setPopoverTarget] = useState(undefined); const [popoverTarget, setPopoverTarget] = useState(undefined);
const widgetManager = useRef(new DefaultWidgetManager(setPopoverOpen, setPopoverTarget)); 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" /> <FormattedMessage id="action.accept" defaultMessage="Accept" />
</Button> </Button>
<Button color="primary" variant="outlined" onClick={props.closeModal}>
<FormattedMessage id="action.cancel" defaultMessage="Cancel" />
</Button>
{props.model.getValue() && props.model.getValue().trim() !== '' && ( {props.model.getValue() && props.model.getValue().trim() !== '' && (
<IconButton <IconButton
onClick={() => { onClick={() => {

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import React from 'react'; 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 ThreeDRotation from '@mui/icons-material/ThreeDRotation';
import Toolbar, { import Toolbar, {
ToolbarButtonOption, ToolbarButtonOption,
@ -80,6 +80,12 @@ const iconFunctionConfig: ActionConfig = {
onClick: jest.fn(), onClick: jest.fn(),
}; };
const submenuOnClickConfig: ActionConfig = {
icon: <ThreeDRotation></ThreeDRotation>,
useClickToClose: true,
options: [config, null, config, null],
};
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@ -192,6 +198,7 @@ describe('Editor Toolbar Submenu', () => {
const item = screen.getByRole('menuitem'); const item = screen.getByRole('menuitem');
fireEvent.mouseOver(item); fireEvent.mouseOver(item);
const clickeableDiv = await screen.findByTestId('custom-render-div'); const clickeableDiv = await screen.findByTestId('custom-render-div');
expect(screen.queryByRole('submenu')).toBeTruthy();
fireEvent.click(clickeableDiv); fireEvent.click(clickeableDiv);
@ -206,6 +213,28 @@ describe('Editor Toolbar Submenu', () => {
expect(screen.queryByRole('submenu')).toBeFalsy(); 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', () => { describe('toolbar menu item', () => {

View File

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

View File

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

View File

@ -7,3 +7,43 @@ context('Maps Page', () => {
// cy.matchImageSnapshot('maps'); // 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": { "info.title": {
"defaultMessage": "Información" "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": { "language.change": {
"defaultMessage": "Cambiar idioma" "defaultMessage": "Cambiar idioma"
}, },

View File

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

View File

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

View File

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

View File

@ -555,6 +555,64 @@
"value": "Información" "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": [ "language.change": [
{ {
"type": 0, "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 AppConfig from '../../classes/app-config';
import exampleMap from '../../classes/client/mock-client/example-map.wxml'; import { LocalStorageManager, Mindmap, XMLSerializerFactory } from '@wisemapping/editor';
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;
};
export const fetchMindmap = async (mapId: number): Promise<Mindmap> => { export const fetchMindmap = async (mapId: number): Promise<Mindmap> => {
let mindmap: Mindmap; let mindmap: Mindmap;

View File

@ -16,10 +16,14 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useEffect } from 'react'; 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 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 { IntlProvider } from 'react-intl';
import AppI18n, { Locales } from '../../classes/app-i18n'; import AppI18n, { Locales } from '../../classes/app-i18n';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -27,17 +31,66 @@ import { hotkeysEnabled } from '../../redux/editorSlice';
import ReactGA from 'react-ga4'; import ReactGA from 'react-ga4';
import { useFetchAccount, useFetchMapById } from '../../redux/clientSlice'; import { useFetchAccount, useFetchMapById } from '../../redux/clientSlice';
import EditorOptionsBuilder from './EditorOptionsBuilder'; import EditorOptionsBuilder from './EditorOptionsBuilder';
import { buildPersistenceManagerForEditor } from './PersistenceManagerUtils';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import AccountMenu from '../maps-page/account-menu';
import MapInfoImpl from '../../classes/editor-map-info'; import MapInfoImpl from '../../classes/editor-map-info';
import { MapInfo } from '@wisemapping/editor'; import { MapInfo } from '@wisemapping/editor';
import { activeInstance } from '../../redux/clientSlice'; import { activeInstance } from '../../redux/clientSlice';
import Client from '../../classes/client'; 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 = { export type EditorPropsType = {
isTryMode: boolean; 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 EditorPage = ({ isTryMode }: EditorPropsType): React.ReactElement => {
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null); 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 ( return (
<FormContainer> <FormContainer maxWidth="xs">
<Typography variant="h4" component="h1"> <Typography variant="h4" component="h1">
<FormattedMessage id="forgot.title" defaultMessage="Reset your password" /> <FormattedMessage id="forgot.title" defaultMessage="Reset your password" />
</Typography> </Typography>

View File

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

View File

@ -1,12 +1,10 @@
import withEmotionStyles from '../../HOCs/withEmotionStyles';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import withStyles from '@mui/styles/withStyles';
const FormContainer = withStyles({ const FormContainer = withEmotionStyles({
root: { padding: '20px 10px 0px 20px',
padding: '20px 10px 0px 20px', maxWidth: '380px !important',
maxWidth: '380px', textAlign: 'center',
textAlign: 'center',
},
})(Container); })(Container);
export default FormContainer; 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 ( return (
<Menu <Menu
anchorEl={anchor} anchorEl={anchor}

View File

@ -1,7 +1,7 @@
import MenuItem from '@mui/material/MenuItem'; 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: { root: {
width: '300px', width: '300px',
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import createStyles from '@mui/styles/createStyles'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import makeStyles from '@mui/styles/makeStyles'; import { useTheme } from '@mui/material/styles';
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() => export const useStyles = () =>
createStyles({ useClasses({
textarea: { textarea: {
width: '100%', width: '100%',
padding: '15px 15px', padding: '15px 15px',
@ -11,5 +12,14 @@ export const useStyles = makeStyles(() =>
textDesc: { textDesc: {
width: '150px', 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 { LabelSelector } from '../../maps-list/label-selector';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list'; import { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list';
import { Interpolation, Theme } from '@emotion/react';
const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => { const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => {
const intl = useIntl(); const intl = useIntl();
@ -62,11 +63,11 @@ const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement
id: 'label.description', id: 'label.description',
defaultMessage: 'Use labels to organize your maps.', defaultMessage: 'Use labels to organize your maps.',
})} })}
PaperProps={{ classes: { root: classes.paper } }} paperCss={classes.paper}
error={error} 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 " /> <FormattedMessage id="label.add-for" defaultMessage="Editing labels for " />
{maps.length > 1 ? ( {maps.length > 1 ? (
<FormattedMessage <FormattedMessage

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,37 @@
import createStyles from '@mui/styles/createStyles'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import makeStyles from '@mui/styles/makeStyles'; import { useTheme } from '@mui/material/styles';
import useClasses from '../../../../theme/useStyles';
export const useStyles = makeStyles(() => export const useStyles = () =>
createStyles({ useClasses({
actionContainer: { actionContainer: {
padding: '10px 0px', padding: '10px 0px',
border: '1px solid rgba(0, 0, 0, 0.12)', border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '8px 8px 0px 0px', borderRadius: '8px 8px 0px 0px',
textAlign: 'center', 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: { textArea: {
width: '730px', [useTheme().breakpoints.up('sm')]: {
margin: '5px 0px', width: '730px',
padding: '10px', margin: '5px 0px',
padding: '10px',
},
}, },
listPaper: { listPaper: {
maxHeight: 200, maxHeight: 200,
@ -21,6 +40,16 @@ export const useStyles = makeStyles(() =>
paper: { paper: {
width: '850px', width: '850px',
minWidth: '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 React, { ErrorInfo, ReactElement, useEffect } from 'react';
import clsx from 'clsx';
import Drawer from '@mui/material/Drawer'; import Drawer from '@mui/material/Drawer';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
@ -20,6 +19,9 @@ import LanguageMenu from './language-menu';
import AppI18n, { Locales } from '../../classes/app-i18n'; import AppI18n, { Locales } from '../../classes/app-i18n';
import ListItemIcon from '@mui/material/ListItemIcon'; 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 AddCircleTwoTone from '@mui/icons-material/AddCircleTwoTone';
import CloudUploadTwoTone from '@mui/icons-material/CloudUploadTwoTone'; import CloudUploadTwoTone from '@mui/icons-material/CloudUploadTwoTone';
@ -41,7 +43,8 @@ import logoIcon from './logo-small.svg';
import poweredByIcon from './pwrdby-white.svg'; import poweredByIcon from './pwrdby-white.svg';
import LabelDeleteConfirm from './maps-list/label-delete-confirm'; import LabelDeleteConfirm from './maps-list/label-delete-confirm';
import ReactGA from 'react-ga4'; 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; export type Filter = GenericFilter | LabelFilter;
@ -61,12 +64,22 @@ interface ToolbarButtonInfo {
} }
const MapsPage = (): ReactElement => { const MapsPage = (): ReactElement => {
const classes = useStyles();
const [filter, setFilter] = React.useState<Filter>({ type: 'all' }); const [filter, setFilter] = React.useState<Filter>({ type: 'all' });
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined); const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null); 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 ... // Reload based on user preference ...
const userLocale = AppI18n.getUserLocale(); const userLocale = AppI18n.getUserLocale();
@ -85,6 +98,7 @@ const MapsPage = (): ReactElement => {
id: 'maps.page-title', id: 'maps.page-title',
defaultMessage: 'My Maps | WiseMapping', defaultMessage: 'My Maps | WiseMapping',
}); });
window.scrollTo(0, 0);
ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: 'Maps List' }); ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: 'Maps List' });
}, []); }, []);
@ -101,6 +115,7 @@ const MapsPage = (): ReactElement => {
const handleMenuClick = (filter: Filter) => { const handleMenuClick = (filter: Filter) => {
queryClient.invalidateQueries('maps'); queryClient.invalidateQueries('maps');
setFilter(filter); setFilter(filter);
mobileOpen && setMobileOpen(false);
}; };
const handleLabelDelete = (id: number) => { 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 ( return (
<IntlProvider <IntlProvider
locale={userLocale.code} locale={userLocale.code}
defaultLocale={Locales.EN.code} defaultLocale={Locales.EN.code}
messages={userLocale.message} messages={userLocale.message}
> >
<div className={classes.root}> <div css={classes.root}>
<AppBar <AppBar
position="fixed" position="fixed"
className={clsx(classes.appBar, { css={[classes.appBar, open && classes.appBarShift]}
[classes.appBarShift]: open,
})}
variant="outlined" variant="outlined"
elevation={0} elevation={0}
> >
<Toolbar> <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 <Tooltip
arrow={true} arrow={true}
title={intl.formatMessage({ title={intl.formatMessage({
@ -179,10 +245,12 @@ const MapsPage = (): ReactElement => {
type="button" type="button"
disableElevation={true} disableElevation={true}
startIcon={<AddCircleTwoTone />} startIcon={<AddCircleTwoTone />}
className={classes.newMapButton} css={classes.newMapButton}
onClick={() => setActiveDialog('create')} onClick={() => setActiveDialog('create')}
> >
<FormattedMessage id="action.new" defaultMessage="New map" /> <span className="message">
<FormattedMessage id="action.new" defaultMessage="New map" />
</span>
</Button> </Button>
</Tooltip> </Tooltip>
@ -200,10 +268,12 @@ const MapsPage = (): ReactElement => {
type="button" type="button"
disableElevation={true} disableElevation={true}
startIcon={<CloudUploadTwoTone />} startIcon={<CloudUploadTwoTone />}
className={classes.importButton} css={classes.importButton}
onClick={() => setActiveDialog('import')} onClick={() => setActiveDialog('import')}
> >
<FormattedMessage id="action.import" defaultMessage="Import" /> <span className="message">
<FormattedMessage id="action.import" defaultMessage="Import" />
</span>
</Button> </Button>
</Tooltip> </Tooltip>
<ActionDispatcher <ActionDispatcher
@ -213,7 +283,7 @@ const MapsPage = (): ReactElement => {
fromEditor fromEditor
/> />
<div className={classes.rightButtonGroup}> <div css={classes.rightButtonGroup as Interpolation<Theme>}>
<LanguageMenu /> <LanguageMenu />
<HelpMenu /> <HelpMenu />
<AccountMenu /> <AccountMenu />
@ -221,44 +291,29 @@ const MapsPage = (): ReactElement => {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Drawer <Drawer
variant="permanent" container={container}
className={clsx(classes.drawer, { variant={'temporary'}
[classes.drawerOpen]: open, open={mobileOpen}
})} onClose={handleMobileDrawerToggle}
classes={{ ModalProps={{
paper: clsx({ keepMounted: true,
[classes.drawerOpen]: open,
}),
}} }}
css={[classes.mobileDrawer, { '& .MuiPaper-root': classes.drawerOpen }]}
> >
<div style={{ padding: '20px 0 20px 15px' }} key="logo"> {drawerItemsList}
<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>
</Drawer> </Drawer>
<main className={classes.content}> <Drawer
<div className={classes.toolbar} /> 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} /> <MapsList filter={filter} />
</main> </main>
</div> </div>
@ -286,24 +341,21 @@ interface ListItemProps {
} }
// https://stackoverflow.com/questions/61486061/how-to-set-selected-and-hover-color-of-listitem-in-mui // https://stackoverflow.com/questions/61486061/how-to-set-selected-and-hover-color-of-listitem-in-mui
const CustomListItem = withStyles({ const CustomListItem = withEmotionStyles({
root: { '&.Mui-selected': {
'&$selected': { backgroundColor: 'rgb(210, 140, 5)',
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', color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
},
},
'&$selected:hover': {
backgroundColor: 'rgb(210, 140, 5)',
color: 'white',
'& .MuiListItemIcon-root': {
color: 'white',
},
}, },
}, },
selected: {},
})(ListItemButton); })(ListItemButton);
const StyleListItem = (props: ListItemProps) => { const StyleListItem = (props: ListItemProps) => {
@ -336,7 +388,9 @@ const StyleListItem = (props: ListItemProps) => {
return ( return (
<CustomListItem selected={isSelected} onClick={(e) => handleOnClick(e, filter)}> <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} /> <ListItemText style={{ color: 'white' }} primary={label} />
{filter.type == 'label' && ( {filter.type == 'label' && (
<ListItemSecondaryAction> <ListItemSecondaryAction>

View File

@ -16,6 +16,8 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText'; import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import { useTheme } from '@mui/material/styles';
import { mobileAppbarButton } from '../style';
const LanguageMenu = (): React.ReactElement => { const LanguageMenu = (): React.ReactElement => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -25,6 +27,8 @@ const LanguageMenu = (): React.ReactElement => {
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const intl = useIntl(); 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. // Todo: For some reasons, in some situations locale is null. More research needed.
const mutation = useMutation( const mutation = useMutation(
@ -68,11 +72,14 @@ const LanguageMenu = (): React.ReactElement => {
variant="outlined" variant="outlined"
disableElevation={true} disableElevation={true}
color="primary" color="primary"
css={{
[smMediaQuery]: mobileAppbarButton,
}}
style={{ borderColor: 'gray', color: 'gray' }} style={{ borderColor: 'gray', color: 'gray' }}
onClick={handleMenu} onClick={handleMenu}
startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />} startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />}
> >
{userLocale.label} <span className="message">{userLocale.label}</span>
</Button> </Button>
</Tooltip> </Tooltip>
<Menu <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 Link from '@mui/material/Link';
import DeleteOutlined from '@mui/icons-material/DeleteOutlined'; import DeleteOutlined from '@mui/icons-material/DeleteOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded'; import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
@ -38,6 +40,12 @@ import { LabelsCell } from './labels-cell';
import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import AppI18n from '../../../classes/app-i18n'; import AppI18n from '../../../classes/app-i18n';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone'; 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(LocalizedFormat);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -134,7 +142,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
padding="checkbox" padding="checkbox"
key="select" key="select"
style={{ width: '20px' }} style={{ width: '20px' }}
className={classes.headerCell} css={classes.headerCell}
> >
<Checkbox <Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
@ -145,7 +153,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
/> />
</TableCell> </TableCell>
<TableCell padding="checkbox" key="starred" className={classes.headerCell}></TableCell> <TableCell padding="checkbox" key="starred" css={classes.headerCell}></TableCell>
{headCells.map((headCell) => { {headCells.map((headCell) => {
return ( return (
@ -153,7 +161,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
key={headCell.id} key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false} sortDirection={orderBy === headCell.id ? order : false}
style={headCell.style} style={headCell.style}
className={classes.headerCell} css={classes.headerCell}
> >
<TableSortLabel <TableSortLabel
active={orderBy === headCell.id} active={orderBy === headCell.id}
@ -163,7 +171,7 @@ function EnhancedTableHead(props: EnhancedTableProps) {
{headCell.label} {headCell.label}
{orderBy === headCell.id && ( {orderBy === headCell.id && (
<span className={classes.visuallyHidden}> <span css={classes.visuallyHidden as Interpolation<Theme>}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'} {order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</span> </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> </TableRow>
</TableHead> </TableHead>
); );
@ -334,7 +342,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
mapId: mapId, mapId: mapId,
el: event.currentTarget, 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) => { const handleStarred = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: number) => {
event.stopPropagation(); event.preventDefault();
starredMultation.mutate(id); starredMultation.mutate(id);
}; };
@ -423,16 +431,16 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
const isSelected = (id: number) => selected.indexOf(id) !== -1; const isSelected = (id: number) => selected.indexOf(id) !== -1;
return ( return (
<div className={classes.root}> <div css={classes.root}>
<ActionChooser <ActionChooser
anchor={activeRowAction?.el} anchor={activeRowAction?.el}
onClose={handleActionMenuClose} onClose={handleActionMenuClose}
mapId={activeRowAction?.mapId} mapId={activeRowAction?.mapId}
/> />
<Paper className={classes.paper} elevation={0}> <Paper css={classes.paper} elevation={0}>
<Toolbar className={classes.toolbar} variant="dense"> <Toolbar css={classes.toolbar} variant="dense">
<div className={classes.toolbarActions}> <div css={classes.toolbarActions}>
{selected.length > 0 && ( {selected.length > 0 && (
<Tooltip <Tooltip
arrow={true} arrow={true}
@ -479,9 +487,24 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
)} )}
</div> </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 <TablePagination
style={{ float: 'right', border: '0', paddingBottom: '5px' }} css={classes.tablePagination as Interpolation<Theme>}
count={mapsInfo.length} count={mapsInfo.length}
rowsPerPageOptions={[]} rowsPerPageOptions={[]}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
@ -490,29 +513,108 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
onRowsPerPageChange={handleChangeRowsPerPage} onRowsPerPageChange={handleChangeRowsPerPage}
component="div" 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> </div>
</Toolbar> </Toolbar>
<TableContainer> <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 <EnhancedTableHead
classes={classes} classes={classes}
numSelected={selected.length} numSelected={selected.length}
@ -555,7 +657,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
selected={isItemSelected} selected={isItemSelected}
style={{ border: '0' }} style={{ border: '0' }}
> >
<TableCell padding="checkbox" className={classes.bodyCell}> <TableCell padding="checkbox" css={classes.bodyCell}>
<Checkbox <Checkbox
checked={isItemSelected} checked={isItemSelected}
inputProps={{ inputProps={{
@ -565,7 +667,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
/> />
</TableCell> </TableCell>
<TableCell padding="checkbox" className={classes.bodyCell}> <TableCell padding="checkbox" css={classes.bodyCell}>
<Tooltip <Tooltip
arrow={true} arrow={true}
title={intl.formatMessage({ title={intl.formatMessage({
@ -584,7 +686,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell css={classes.bodyCell}>
<Tooltip <Tooltip
arrow={true} arrow={true}
title={intl.formatMessage({ title={intl.formatMessage({
@ -604,7 +706,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}> <TableCell css={[classes.bodyCell, classes.labelsCell as CSSObject]}>
<LabelsCell <LabelsCell
labels={row.labels} labels={row.labels}
onDelete={(lbl) => { onDelete={(lbl) => {
@ -613,9 +715,9 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
/> />
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}>{row.createdBy}</TableCell> <TableCell css={classes.bodyCell}>{row.createdBy}</TableCell>
<TableCell className={classes.bodyCell}> <TableCell css={classes.bodyCell}>
<Tooltip <Tooltip
arrow={true} arrow={true}
title={intl.formatMessage( title={intl.formatMessage(
@ -634,7 +736,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell css={classes.bodyCell}>
<Tooltip <Tooltip
arrow={true} arrow={true}
title={intl.formatMessage({ 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 { alpha, useTheme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles'; import useClasses from '../../../theme/useStyles';
import makeStyles from '@mui/styles/makeStyles';
export const useStyles = makeStyles(() => export const useStyles = () => {
createStyles({ const theme = useTheme();
const smMediaQuery = theme.breakpoints.down('sm');
return useClasses({
root: { root: {
width: '100%', width: '100%',
}, },
paper: { paper: {
width: '100%', width: '100%',
marginBottom: useTheme().spacing(2), marginBottom: theme.spacing(2),
},
cards: {
display: 'none',
[smMediaQuery]: {
display: 'block',
},
}, },
table: { table: {
[smMediaQuery]: {
display: 'none',
},
minWidth: 750, minWidth: 750,
'& tr:nth-child(even)': { '& tr:nth-of-type(2n)': {
background: 'white', background: 'white',
}, },
'& tr:nth-child(odd)': { '& tr:nth-of-type(2n+1)': {
background: 'rgba(221, 221, 221, 0.35)', background: 'rgba(221, 221, 221, 0.35)',
}, },
// '&:hover tr': {
// backgroundColor: 'rgba(150, 150, 150, 0.7)',
// }
}, },
headerCell: { headerCell: {
background: 'white', background: 'white',
@ -61,22 +70,31 @@ export const useStyles = makeStyles(() =>
flexGrow: 1, flexGrow: 1,
paddingLeft: '23px;', paddingLeft: '23px;',
}, },
toolbarListActions: {
flexGrow: 1,
},
search: { search: {
borderRadius: 9, borderRadius: 9,
backgroundColor: alpha(useTheme().palette.common.white, 0.15), backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': { '&:hover': {
backgroundColor: alpha(useTheme().palette.common.white, 0.25), backgroundColor: alpha(theme.palette.common.white, 0.25),
}, },
margin: '10px 0px', margin: '10px 0px',
width: '100%', width: '100%',
[useTheme().breakpoints.up('sm')]: { [theme.breakpoints.up('sm')]: {
marginLeft: useTheme().spacing(1), marginLeft: theme.spacing(1),
width: 'auto', width: 'auto',
}, },
float: 'left',
[smMediaQuery]: {
width: '50%',
},
},
tablePagination: {
float: 'right', float: 'right',
border: '0',
paddingBottom: '5px',
[smMediaQuery]: {
width: '50%',
overflow: 'hidden',
},
}, },
searchIcon: { searchIcon: {
padding: '6px 0 0 5px', padding: '6px 0 0 5px',
@ -93,19 +111,22 @@ export const useStyles = makeStyles(() =>
float: 'right', float: 'right',
}, },
searchInputInput: { searchInputInput: {
// padding: theme.spacing(1, 1, 1, 0), '& .MuiInputBase-input': {
// vertical padding + font size from searchIcon border: '1px solid #ffa800',
border: '1px solid #ffa800', borderRadius: 4,
borderRadius: 4, paddingLeft: `calc(1em + ${theme.spacing(4)})`,
paddingLeft: `calc(1em + ${useTheme().spacing(4)})`, transition: theme.transitions.create('width'),
transition: useTheme().transitions.create('width'), width: '100%',
width: '100%', [theme.breakpoints.up('sm')]: {
[useTheme().breakpoints.up('sm')]: { width: '12ch',
width: '12ch', '&:focus': {
'&:focus': { width: '20ch',
width: '20ch', },
}, },
}, },
}, },
}), cardHeader: {
); padding: '4px',
},
});
};

View File

@ -1,10 +1,45 @@
import createStyles from '@mui/styles/createStyles'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import makeStyles from '@mui/styles/makeStyles'; 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(() => const closedMixin = (theme: Theme): CSSObject => ({
createStyles({ 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: { root: {
display: 'flex', display: 'flex',
}, },
@ -14,29 +49,70 @@ export const useStyles = makeStyles(() =>
appBarShift: { appBarShift: {
marginLeft: drawerWidth, marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`,
[smMediaQuery]: {
width: '100%',
},
}, },
newMapButton: { newMapButton: {
marginRight: 10, marginRight: 10,
minWidth: '130px', minWidth: '130px',
[smMediaQuery]: mobileAppbarButton,
}, },
importButton: { importButton: {
marginRight: 10, marginRight: 10,
minWidth: '130px', minWidth: '130px',
[smMediaQuery]: mobileAppbarButton,
}, },
rightButtonGroup: { rightButtonGroup: {
marginRight: 10, marginRight: 10,
flexGrow: 10, flexGrow: 10,
textAlign: 'right', textAlign: 'right',
minWidth: '280px', minWidth: '280px',
[smMediaQuery]: {
minWidth: 'unset',
marginRight: 0,
},
}, },
drawer: { drawer: {
width: drawerWidth, width: drawerWidth,
flexShrink: 0, flexShrink: 0,
[smMediaQuery]: {
display: 'none',
},
whiteSpace: 'nowrap', 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: { drawerOpen: {
background: '#ffa800', background: '#ffa800',
width: drawerWidth, width: drawerWidth,
[smMediaQuery]: {
width: 300,
},
}, },
toolbar: { toolbar: {
display: 'flex', display: 'flex',
@ -47,5 +123,5 @@ export const useStyles = makeStyles(() =>
flexGrow: 1, flexGrow: 1,
padding: '24px 0px', 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;