mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-22 14:47:56 +01:00
Export fixes.
This commit is contained in:
parent
a502498c42
commit
7c9effaa5d
25
packages/editor/src/components/action-button/index.ts
Normal file
25
packages/editor/src/components/action-button/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ActionButton = styled.div`
|
||||
cursor: pointer;
|
||||
margin: 0px 10px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
padding: 10px 25px;
|
||||
font-size: 15px;
|
||||
min-width: 64px;
|
||||
box-sizing: border-box;
|
||||
font-weight: 600;
|
||||
border-radius: 9px;
|
||||
color: white;
|
||||
background-color: #ffa800;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
}
|
||||
`;
|
||||
|
||||
export default ActionButton;
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyledLogo } from './styled';
|
||||
import { StyledLogo, Notifier } from './styled';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import KeyboardSvg from '../../../images/keyboard.svg';
|
||||
import AddSvg from '../../../images/add.svg';
|
||||
import MinusSvg from '../../../images/minus.svg';
|
||||
import CenterFocusSvg from '../../../images/center_focus.svg';
|
||||
import ActionButton from '../action-button';
|
||||
|
||||
export type FooterPropsType = {
|
||||
showTryPanel?: boolean;
|
||||
@ -35,15 +36,15 @@ const Footer = ({ showTryPanel }: FooterPropsType): React.ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
<StyledLogo id="bottom-logo"></StyledLogo>
|
||||
<div id="headerNotifier"></div>
|
||||
<Notifier id="headerNotifier"></Notifier>
|
||||
{showTryPanel && (
|
||||
<div id="tryInfoPanel">
|
||||
<p>{intl.formatMessage({ id: 'editor.try-welcome' })}</p>
|
||||
<p>{intl.formatMessage({ id: 'editor.try-welcome-description' })}</p>
|
||||
<a href="/c/registration">
|
||||
<div className="actionButton">
|
||||
<ActionButton>
|
||||
{intl.formatMessage({ id: 'login.signup', defaultMessage: 'Sign Up' })}
|
||||
</div>
|
||||
</ActionButton>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
@ -15,4 +15,17 @@ export const StyledLogo = styled.div`
|
||||
background: url(${LogoTextBlackSvg}) no-repeat;
|
||||
width: 90px;
|
||||
height: 40px;
|
||||
`
|
||||
`;
|
||||
|
||||
export const Notifier = styled.div`
|
||||
border: 1px solid rgb(241, 163, 39);
|
||||
background-color: rgb(252, 235, 192);
|
||||
border-radius: 3px;
|
||||
position: fixed;
|
||||
padding: 5px 9px;
|
||||
color: back;
|
||||
white-space: nowrap;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
bottom: 10px;
|
||||
`;
|
||||
|
@ -25,6 +25,9 @@ import HistorySvg from '../../../images/history.svg';
|
||||
import PrintSvg from '../../../images/print.svg';
|
||||
import AccountSvg from '../../../images/account.svg';
|
||||
|
||||
import { HeaderContainer, ToolbarButton, ToolbarButtonExt, ToolbarRightContainer } from './styled';
|
||||
import ActionButton from '../action-button';
|
||||
|
||||
export type ToolbarActionType = 'export' | 'publish' | 'history' | 'print' | 'share';
|
||||
|
||||
export type ToolbarPropsType = {
|
||||
@ -40,100 +43,118 @@ export default function Toolbar({
|
||||
}: ToolbarPropsType): React.ReactElement {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<div id="toolbar">
|
||||
<div id="backToList">
|
||||
<img src={BackIconSvg} />
|
||||
</div>
|
||||
{!memoryPersistence && (
|
||||
<div id="persist" className="buttonContainer">
|
||||
<div id="save" className="buttonOn">
|
||||
<ToolbarButton id="save" className="buttonOn">
|
||||
<img src={SaveSvg} />
|
||||
</div>
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
)}
|
||||
{!readOnlyMode && (
|
||||
<>
|
||||
<div id="edit" className="buttonContainer">
|
||||
<div id="undoEdition" className="buttonOn">
|
||||
<ToolbarButton id="undoEdition" className="buttonOn">
|
||||
<img src={UndoSvg} />
|
||||
</div>
|
||||
<div id="redoEdition" className="buttonOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="redoEdition" className="buttonOn">
|
||||
<img src={RedoSvg} />
|
||||
</div>
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
<div id="nodeStyle" className="buttonContainer">
|
||||
<div id="addTopic" className="buttonOn">
|
||||
<ToolbarButton id="addTopic" className="buttonOn">
|
||||
<img src={TopicAddSvg} />
|
||||
</div>
|
||||
<div id="deleteTopic" className="buttonOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="deleteTopic" className="buttonOn">
|
||||
<img src={TopicDeleteSvg} />
|
||||
</div>
|
||||
<div id="topicBorder" className="buttonExtOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButtonExt id="topicBorder" className="buttonExtOn">
|
||||
<img src={TopicBorderSvg} />
|
||||
</div>
|
||||
<div id="topicColor" className="buttonExtOn">
|
||||
</ToolbarButtonExt>
|
||||
<ToolbarButtonExt id="topicColor" className="buttonExtOn">
|
||||
<img src={TopicColorSvg} />
|
||||
</div>
|
||||
<div id="topicShape" className="buttonExtOn">
|
||||
</ToolbarButtonExt>
|
||||
<ToolbarButtonExt id="topicShape" className="buttonExtOn">
|
||||
<img src={TopicShapeSvg} />
|
||||
</div>
|
||||
</ToolbarButtonExt>
|
||||
</div>
|
||||
<div id="font" className="buttonContainer">
|
||||
<div id="fontFamily" className="buttonOn">
|
||||
<ToolbarButton id="fontFamily" className="buttonOn">
|
||||
<img src={FontTypeSvg} />
|
||||
</div>
|
||||
<div id="fontSize" className="buttonExtOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButtonExt id="fontSize" className="buttonExtOn">
|
||||
<img src={FontSizeSvg} />
|
||||
</div>
|
||||
<div id="fontBold" className="buttonOn">
|
||||
</ToolbarButtonExt>
|
||||
<ToolbarButton id="fontBold" className="buttonOn">
|
||||
<img src={FontBoldSvg} />
|
||||
</div>
|
||||
<div id="fontItalic" className="buttonOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="fontItalic" className="buttonOn">
|
||||
<img src={FontItalicSvg} />
|
||||
</div>
|
||||
<div id="fontColor" className="buttonExtOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButtonExt id="fontColor" className="buttonExtOn">
|
||||
<img src={FontColorSvg} />
|
||||
</div>
|
||||
</ToolbarButtonExt>
|
||||
</div>
|
||||
<div id="nodeContent" className="buttonContainer">
|
||||
<div id="topicIcon" className="buttonExtOn">
|
||||
<ToolbarButtonExt id="topicIcon" className="buttonExtOn">
|
||||
<img src={TopicIconSvg} />
|
||||
</div>
|
||||
<div id="topicNote" className="buttonOn">
|
||||
</ToolbarButtonExt>
|
||||
<ToolbarButton id="topicNote" className="buttonOn">
|
||||
<img src={TopicNoteSvg} />
|
||||
</div>
|
||||
<div id="topicLink" className="buttonOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="topicLink" className="buttonOn">
|
||||
<img src={TopicLinkSvg} />
|
||||
</div>
|
||||
<div id="topicRelation" className="buttonOn">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="topicRelation" className="buttonOn">
|
||||
<img src={TopicRelationSvg} />
|
||||
</div>
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
<div id="separator" className="buttonContainer"></div>
|
||||
</>
|
||||
)}
|
||||
{!memoryPersistence && (
|
||||
<div id="toolbarRight">
|
||||
<div id="export" className="buttonOn" onClick={() => onAction('export')}>
|
||||
<ToolbarRightContainer>
|
||||
<ToolbarButton
|
||||
id="export"
|
||||
className="buttonOn"
|
||||
onClick={() => onAction('export')}
|
||||
>
|
||||
<img src={ExportSvg} />
|
||||
</div>
|
||||
<div id="publishIt" className="buttonOn" onClick={() => onAction('publish')}>
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
id="publishIt"
|
||||
className="buttonOn"
|
||||
onClick={() => onAction('publish')}
|
||||
>
|
||||
<img src={PublicSvg} />
|
||||
</div>
|
||||
<div id="history" className="buttonOn" onClick={() => onAction('history')}>
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
id="history"
|
||||
className="buttonOn"
|
||||
onClick={() => onAction('history')}
|
||||
>
|
||||
<img src={HistorySvg} />
|
||||
</div>
|
||||
<div id="print" className="buttonOn" onClick={() => onAction('print')}>
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
id="print"
|
||||
className="buttonOn"
|
||||
onClick={() => onAction('print')}
|
||||
>
|
||||
<img src={PrintSvg} />
|
||||
</div>
|
||||
<div id="account">
|
||||
</ToolbarButton>
|
||||
<ToolbarButton id="account">
|
||||
<img src={AccountSvg} />
|
||||
</div>
|
||||
<div id="share" className="actionButton" onClick={() => onAction('share')}>
|
||||
{ intl.formatMessage({ id: 'action.share', defaultMessage: 'Share' }) }
|
||||
</div>
|
||||
</div>
|
||||
</ToolbarButton>
|
||||
<ActionButton onClick={() => onAction('share')}>
|
||||
{intl.formatMessage({ id: 'action.share', defaultMessage: 'Share' })}
|
||||
</ActionButton>
|
||||
</ToolbarRightContainer>
|
||||
)}
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
50
packages/editor/src/components/toolbar/styled.ts
Normal file
50
packages/editor/src/components/toolbar/styled.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
background: #202020;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const ToolbarContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const ToolbarButton = styled.div`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
z-index: 4;
|
||||
margin-top: 3px;
|
||||
padding-top: 2px;
|
||||
padding-left: 2px;
|
||||
margin-left: 3px;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export const ToolbarButtonExt = styled(ToolbarButton)`
|
||||
width: 40px;
|
||||
text-align: left;
|
||||
padding-left: 5px;
|
||||
`;
|
||||
|
||||
export const AccountButton = styled.div`
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
`;
|
||||
|
||||
export const ToolbarRightContainer = styled.div`
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
@ -2,7 +2,14 @@ import React from 'react';
|
||||
import Toolbar, { ToolbarActionType } from './components/toolbar';
|
||||
import Footer from './components/footer';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { $notify, buildDesigner, LocalStorageManager, PersistenceManager, RESTPersistenceManager, DesignerOptionsBuilder } from '@wisemapping/mindplot';
|
||||
import {
|
||||
$notify,
|
||||
buildDesigner,
|
||||
LocalStorageManager,
|
||||
PersistenceManager,
|
||||
RESTPersistenceManager,
|
||||
DesignerOptionsBuilder,
|
||||
} from '@wisemapping/mindplot';
|
||||
|
||||
declare global {
|
||||
var memoryPersistence: boolean;
|
||||
@ -28,9 +35,8 @@ export type EditorPropsType = {
|
||||
};
|
||||
|
||||
const initMindplot = () => {
|
||||
|
||||
// Change page title ...
|
||||
document.title = `${global.mapTitle} | WiseMapping `
|
||||
document.title = `${global.mapTitle} | WiseMapping `;
|
||||
|
||||
// Configure persistence manager ...
|
||||
let persistence;
|
||||
@ -44,7 +50,8 @@ const initMindplot = () => {
|
||||
});
|
||||
} else {
|
||||
persistence = new LocalStorageManager(
|
||||
`/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${!global.isAuth ? '-pub' : ''
|
||||
`/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${
|
||||
!global.isAuth ? '-pub' : ''
|
||||
}`,
|
||||
true
|
||||
);
|
||||
@ -58,7 +65,11 @@ const initMindplot = () => {
|
||||
readOnly: Boolean(global.readOnly || false),
|
||||
mapId: String(global.mapId),
|
||||
container: 'mindplot',
|
||||
zoom: zoomParam || (global.userOptions?.zoom != undefined ? Number.parseFloat(global.userOptions.zoom as string) : 0.8),
|
||||
zoom:
|
||||
zoomParam ||
|
||||
(global.userOptions?.zoom != undefined
|
||||
? Number.parseFloat(global.userOptions.zoom as string)
|
||||
: 0.8),
|
||||
locale: global.locale,
|
||||
});
|
||||
|
||||
@ -83,18 +94,15 @@ export default function Editor({
|
||||
locale = 'en',
|
||||
onAction,
|
||||
}: EditorPropsType): React.ReactElement {
|
||||
|
||||
React.useEffect(initCallback, []);
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} defaultLocale="en" messages={{}}>
|
||||
<div id="header">
|
||||
<Toolbar
|
||||
memoryPersistence={memoryPersistence}
|
||||
readOnlyMode={readOnlyMode}
|
||||
onAction={onAction}
|
||||
/>
|
||||
</div>
|
||||
<div id="mindplot"></div>
|
||||
<Footer showTryPanel={memoryPersistence} />
|
||||
</IntlProvider>
|
||||
|
@ -1,80 +1,6 @@
|
||||
@import "toolbar.css";
|
||||
|
||||
div#header {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
background: #202020;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
div#headerActions span {
|
||||
border-bottom: 3px solid rgb(247, 201, 49);
|
||||
}
|
||||
|
||||
div#headerActions a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div#headerNotifier {
|
||||
border: 1px solid rgb(241, 163, 39);
|
||||
background-color: rgb(252, 235, 192);
|
||||
border-radius: 3px;
|
||||
position: fixed;
|
||||
padding: 5px 9px;
|
||||
color: back;
|
||||
white-space: nowrap;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
div#toolbarRight {
|
||||
float: right;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
margin: 6px 10px;
|
||||
height: 100%;
|
||||
}
|
||||
#account {
|
||||
float: right;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#account >img {
|
||||
width: 36x;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
#accountSettingsPanel{
|
||||
padding:10px 10px;
|
||||
}
|
||||
|
||||
#share {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin: 0px 10px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
padding: 10px 25px;
|
||||
font-size: 15px;
|
||||
min-width: 64px;
|
||||
box-sizing: border-box;
|
||||
font-weight: 600;
|
||||
border-radius: 9px;
|
||||
color: white;
|
||||
background-color: #ffa800;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
@ -40,7 +38,4 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
]
|
||||
}
|
||||
|
@ -36,7 +36,10 @@ const playgroundConfig = {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new CleanWebpackPlugin({
|
||||
dangerouslyAllowCleanPatternsOutsideProject: true,
|
||||
dry: false,
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'test/playground/map-render/images/favicon.ico', to: 'favicon.ico' },
|
||||
|
@ -1,5 +1,6 @@
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
const prodConfig = {
|
||||
optimization: {
|
||||
@ -8,9 +9,12 @@ const prodConfig = {
|
||||
},
|
||||
externals: {
|
||||
react: 'react',
|
||||
reactDOM: 'react-dom',
|
||||
reactIntl: 'react-intl',
|
||||
"react-dom": 'react-dom',
|
||||
"react-intl": 'react-intl',
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = merge(common, prodConfig);
|
||||
|
@ -20,8 +20,8 @@ import { $assert } from '@wisemapping/core-js';
|
||||
import $ from 'jquery';
|
||||
|
||||
class ToolbarNotifier {
|
||||
constructor() {
|
||||
this.container = $('#headerNotifier');
|
||||
get container() {
|
||||
return $('#headerNotifier');
|
||||
}
|
||||
|
||||
hide() {
|
||||
@ -31,7 +31,7 @@ class ToolbarNotifier {
|
||||
logMessage(msg) {
|
||||
$assert(msg, 'msg can not be null');
|
||||
// In case of print,embedded no message is displayed ....
|
||||
if (this.container && !this.container.data('transitioning')) {
|
||||
if (this.container && this.container.length && !this.container.data('transitioning')) {
|
||||
this.container.data('transitioning', true);
|
||||
this.container.text(msg);
|
||||
this.container.css({
|
||||
|
@ -61,6 +61,7 @@
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"@wisemapping/editor": "^0.4.0",
|
||||
"@wisemapping/mindplot": "^5.0.2",
|
||||
"axios": "^0.21.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"react": "^17.0.0",
|
||||
|
@ -10,7 +10,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import { Designer, TextExporterFactory, ImageExporterFactory, Exporter, Mindmap, RESTPersistenceManager } from '@wisemapping/mindplot';
|
||||
import { Designer, TextExporterFactory, ImageExporterFactory, Exporter, Mindmap, LocalStorageManager } from '@wisemapping/mindplot';
|
||||
|
||||
type ExportFormat = 'svg' | 'jpg' | 'png' | 'txt' | 'mm' | 'wxml' | 'xls' | 'md';
|
||||
type ExportGroup = 'image' | 'document' | 'mindmap-tool';
|
||||
@ -29,6 +29,7 @@ const ExportDialog = ({
|
||||
}: ExportDialogProps): React.ReactElement => {
|
||||
const intl = useIntl();
|
||||
const [submit, setSubmit] = React.useState<boolean>(false);
|
||||
const { map } = fetchMapById(mapId);
|
||||
|
||||
const [exportGroup, setExportGroup] = React.useState<ExportGroup>(
|
||||
enableImgExport ? 'image' : 'document'
|
||||
@ -83,15 +84,11 @@ const ExportDialog = ({
|
||||
mindmap = designer.getMindmap();
|
||||
} else {
|
||||
// Load mindmap ...
|
||||
const persistence = new RESTPersistenceManager({
|
||||
documentUrl: '/c/restful/maps/{id}/document',
|
||||
revertUrl: '/c/restful/maps/{id}/history/latest',
|
||||
lockUrl: '/c/restful/maps/{id}/lock',
|
||||
timestamp: global.lockTimestamp,
|
||||
session: global.lockSession,
|
||||
});
|
||||
mindmap = persistence.load(global.mapId)
|
||||
|
||||
const persistence = new LocalStorageManager(
|
||||
`/c/restful/maps/{id}/document/xml`,
|
||||
true
|
||||
);
|
||||
mindmap = persistence.load(mapId.toString());
|
||||
}
|
||||
|
||||
let exporter: Exporter;
|
||||
@ -116,7 +113,6 @@ const ExportDialog = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { map } = fetchMapById(mapId);
|
||||
if (submit) {
|
||||
exporter(exportFormat)
|
||||
.then((url: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user