Commit ad98cba4 by GGbong

菜单自动化

1 parent c5d06791
const { override, addWebpackAlias } = require('customize-cra')
const path = require('path')
module.exports = override(
//配置路径别名
addWebpackAlias({
'@': path.resolve(__dirname, './src'),
'_c': path.resolve(__dirname, './src/components')
})
)
\ No newline at end of file
...@@ -3,17 +3,25 @@ ...@@ -3,17 +3,25 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@loadable/component": "^5.13.1",
"@rematch/core": "^1.4.0",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0", "@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
"antd": "^4.4.2",
"axios": "^0.19.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "3.4.1" "react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"xbd-cookie": "^0.5.11",
"xbd-system-react": "0.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-app-rewired start",
"build": "react-scripts build", "build": "react-app-rewired build",
"test": "react-scripts test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
...@@ -30,5 +38,11 @@ ...@@ -30,5 +38,11 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"cross-env": "^7.0.2",
"customize-cra": "^1.0.0",
"node-sass": "^4.14.1",
"react-app-rewired": "^2.1.6"
} }
} }
...@@ -24,10 +24,10 @@ ...@@ -24,10 +24,10 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>信巴迪-卖家工作台</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>你的浏览器暂不支持打开该平台,请更换其他浏览器重试</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
......
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
import React from 'react'; import React from 'react';
import logo from './logo.svg'; import RouterView from './router'
import './App.css'; import { Provider } from 'react-redux'
import store from './store'
function App() { function App() {
return ( return (
<div className="App"> <Provider store={store}>
<header className="App-header"> <RouterView></RouterView>
<img src={logo} className="App-logo" alt="logo" /> </Provider>
<p> )
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
} }
export default App; export default App;
import './style.scss'
import View from './view'
import connect from '@/store/modules/route/connect'
// 订阅数据
export default connect(View)
.site-layout{
.site-layout-background {
background: #1890FF;
height: 60px;
}
.icon{
width: 18px;
height: 18px;
margin-right: 10px;
}
}
\ No newline at end of file
import React, { useState, useEffect } from 'react'
import { NavLink } from 'react-router-dom'
import { Layout, Menu } from 'antd'
import { getAllRouter } from '@/libs/utilts'
import { APP } from '../../config'
const { Header, Content, Sider } = Layout
const { SubMenu } = Menu
const theme = "light"
const Item = ({url, children, ...props}) => {
return (
<Menu.Item {...props} >
<NavLink to={{ pathname: url }}>{children}</NavLink>
</Menu.Item>
)
}
const Page = ({ children, userRoute = [], location }) => {
const [collapsed, setCollapsed] = useState(false)
const [index, setIdex] = useState([])
const onCollapse = () => setCollapsed(!collapsed)
useEffect(() => {
const allRouter = getAllRouter(userRoute)
const route = allRouter.find(e => e.url === location.pathname)
if (route)
setIdex([route.url])
else
setIdex(['/'])
}, [location, userRoute])
return (
<Layout style={{ minHeight: '100vh' }} className="site-layout" >
<Sider theme={theme} collapsible collapsed={collapsed} onCollapse={onCollapse}>
<Header className="site-layout-background" style={{ padding: 0 }} />
<Menu theme={theme} mode="inline" selectedKeys={index} >
{userRoute.map(e => {
return (
<SubMenu key={e.name} icon={<img className="icon" src={APP.STATIS_URL + e.icon} />} title={e.name}>
{e.childMenus.map(child => <Item key={child.url} url={child.url} >{child.name}</Item>)}
</SubMenu>
)
})}
</Menu>
</Sider>
<Layout>
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px', display: 'flex' }}>
<div style={{ flex: 1, backgroundColor: '#fff' }}>
{children}
</div>
</Content>
</Layout>
</Layout>
)
}
export default Page
const getProcessValue = (dev, ceshi, prod) => {
return process.env.NODE_ENV === 'development' ? dev : (process.ceshi ? ceshi : prod)
}
const USER_CONFIG = {
EbankTips: '因工行只支持360、IE、Safari浏览器,请将以下链接复制至360、IE、Safari浏览器打开进行支付'
}
const APP = {
/** @description token存储的名称 */
tokenName: 'token',
/** @description token在存储的时长,默认无操作后的30分钟。0 表示关闭浏览器即删除。单位 秒 */
tokenExpires: 0,
/** @description token使用的存储类型,可选值: coookie, session, 默认值是session*/
tokenType: 'cookie',
/** @description 是否只限制此子域名可以访问此token,注意这会影响到主域名和其他子域名进行token共享默认false*/
tokenDomain: process.env.NODE_ENV === 'development' ? 'localhost' :'.b2bwings.com',
/**@description 页面加载出现loading条,默认值 true*/
loading: true,
/**@description 初始的HTTP请求的全局header头*/
header: { },
/**@description API基础请求地址*/
baseUrl: {
dev: 'http://gateway-dev.b2bwings.com/',
pro: 'https://gateway.b2bwings.com/'
},
/**@description 应用的登陆API的PATH*/
loginPath: '/user/sysLogin/login',
/**@description 应用的退出API的PATH*/
logoutPath: '/v1/vendors/logout',
/**@description 401页面地址,用于提醒用户登陆状态失效*/
error404: '/404',
/**@description 菜单缓存本地的名称*/
MENU_NAEM: 'SYS_MENU',
/**@description 资源组缓存本地的名称*/
CODES_NAME: 'SYS_CODES',
/**@description 路由历史记录缓存本地的名称*/
HISTORY_NAME: 'SYS_HISTORY',
/**@description 静态资源目录,必须。用于菜单图标显示*/
STATIS_URL: 'https://b2bwings-system-image.oss-cn-shenzhen.aliyuncs.com/',
/**@description 信让的跳转URL列表 */
domainWhiteList: [
'/.*',
'http://localhost',
'http://xbd-dev.b2bwings.com',
'https://xbd-dev.b2bwings.com',
'http://xbd.b2bwings.com/',
'https://xbd.b2bwings.com/',
'https://ufa.b2bwings.com/',
'http://ufa-dev.b2bwings.com/',
'https://bwfs.oss-cn-shenzhen.aliyuncs.com',
'https://supplier-dev.b2bwings.com',
'http://supplier-dev.b2bwings.com',
'https://supplier.b2bwings.com',
'http://supplier.b2bwings.com',
'https://seller-dev.b2bwings.com',
'http://seller-dev.b2bwings.com',
'https://seller.b2bwings.com',
'http://seller.b2bwings.com',
'https://mall-dev.b2bwings.com',
'http://mall-dev.b2bwings.com',
'https://mall.b2bwings.com',
'http://mall.b2bwings.com'
],
/** 独立服务平台,主题颜色不同 */
singeService: [
{
name: '供应商工作台',
host: getProcessValue(['localhost:8087'],
['supplier-dev.b2bwings.com'],
['supplier.b2bwings.com']
),
productId: 100007,
less: 'mall'
},
{
name: '卖家工作台',
host: getProcessValue(['localhost:8085'],
['seller-dev.b2bwings.com'],
['seller.b2bwings.com']
),
productId: 100005,
less: 'mall'
},
{
name: '商城',
host: getProcessValue(['localhost:8086'],
['mall-dev.b2bwings.com'],
['mall.b2bwings.com']
),
productId: 100006,
less: 'mall'
}
],
host: getProcessValue('localhost:8088', 'xbd-dev.b2bwings.com', 'xbd.b2bwings.com'),
// 财务业务类型
FINANCIAL_BUSINESS_TYPE: (val) => {
if(val===null||val===''||val === undefined) return ''
let result = {
T100:'平台信息服务',
T201:'物流运输服务',
T202:'预付运费充值',
T300:'充值',
T301:'结算',
T302:'转账',
T303:'提现',
T304:'提现-退款',
T400:'支付手续费',
T401:'手续费-退款',
T402:'支付-退款',
T500:'渠道营销服务',
T600:'在途转可用',
T700:'退款手续费',
T800:'资金分账',
T900:'保证金缴纳',
T901:'保证金退款',
T902:'保证金抵扣',
}
return result['T'+val]?result['T'+val]:null
},
// 财务收支类型
FINANCIAL_RECEIVE_AND_PAY_TYPE: (val) => {
if(val===null||val===''||val === undefined) return ''
let types = {
t100:'收入',
t101:'支出',
t102:'预收',
t103:'预付',
t104:'应收',
t105:'应付',
t106:'核销',
t107:'在途转可用',
t108:'退款收入',
t109:'退款支出'
}
return types['t'+val]?types['t'+val]:''
},
/**@description 其他菜单的域名*/
othreDomain: getProcessValue('http://seller-dev.b2bwings.com', 'http://seller-dev.b2bwings.com', 'https://seller.b2bwings.com')
}
export default { USER_CONFIG, APP }
export { USER_CONFIG, APP }
...@@ -3,15 +3,14 @@ import ReactDOM from 'react-dom'; ...@@ -3,15 +3,14 @@ import ReactDOM from 'react-dom';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
import 'antd/dist/antd.css';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <App />,
<App />
</React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls. // unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA // Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister(); process.env.NODE_ENV === 'development' ? serviceWorker.unregister() : serviceWorker.register()
import HttpRequest from '@/libs/http-request'
import { APP } from '@/config'
const baseUrl = process.env.NODE_ENV === 'development' || process.ceshi ? APP.baseUrl.dev : APP.baseUrl.pro
const http = new HttpRequest(baseUrl)
export default http
\ No newline at end of file
import axios, { CancelToken } from 'axios'
import { APP } from '@/config'
import store from '@/store'
import { Modal } from 'antd'
class HttpRequest {
constructor (baseUrl, loginUrl = APP.loginPath) {
this.baseUrl = baseUrl
this.loginUrl = loginUrl
this.header = { ...APP.header }
this.instance = null
this.init()
}
/**初始化 */
init() {
let instance = axios.create({ baseURL: this.baseUrl, headers: this.header })
this.interceptors(instance)
this.instance = instance
}
/**绑定请求拦截器 */
interceptors (instance) {
// 请求
instance.interceptors.request.use(config => {
// config.headers['Authorization'] = getToken()
return config
}, error => {
if (process.env.NODE_ENV === 'development') console.error(error)
return Promise.reject(error)
})
// 响应
instance.interceptors.response.use(res => {
if (res.headers['content-type'] && res.headers['content-type'].indexOf('download') !== -1) return res
if (res.headers['content-type'] && res.headers['content-type'].indexOf('excel') !== -1) return res
if (res.data.type === 'multipart/form-data' && res.data.size) return res
if (res.headers['content-type'] && res.headers['content-disposition']) return res
return this.handleRespone(res)
}, error => {
if (process.env.NODE_ENV === 'development') console.error(error)
return Promise.reject(error.response ?
`发生错误!状态码:${error.response.status}${error.response.message||error.response.data.message}`
: error.message + "")
})
}
/**请求 */
async request (options) {
return this.instance(options)
}
/**
* 可取消的请求,返回一个source对象对象。可通过 source.cancel(msg) 取消此请求
* @param {*} options
* @param {function} successCallback
* @param {function} errorCallback
*/
sourceRequest(options, successCallback, errorCallback) {
const source = CancelToken.source()
options = Object.assign(options, { cancelToken: source.token })
this.instance(options)
.then(successCallback)
.catch(errorCallback)
return source
}
/**
* 设置请求头
* @param {string} key
* @param {any} value
*/
setHeader(key, value) {
this.instance.defaults.headers.common[key] = value
}
/**
* 设置URL
* @param {string} url
*/
setBaseUrl(url) {
this.instance.defaults.baseURL = url
}
/**
* 处理响用体
* @param {response} res
*/
handleRespone(res) {
if (!res.data) return res
const { code, message, data, list } = res.data
if(typeof code === 'undefined') return data
if(typeof list !== 'undefined' && typeof data === 'undefined') return list
switch(code) {
case 0:
return data ? data : { data, list }
case "0":
return data ? data : { data, list }
case 200:
if (res.config.url.indexOf(this.loginUrl) !== -1) {
this.saveLoginInfo(data.jwt, data.sessionid, data.user.vid, res.config.data) //保存登陆数据
}
return Array.isArray(data)?data:(typeof data === 'object' ? {message, code, ...data} : {data, message})
// case 400001:
// return Array.isArray(data)?data:(typeof data === 'object' ? {message, code, ...data} : {data, message})
case 401:
if (process.env.NODE_ENV === 'development') console.error(`发生错误,错误代码: ${code};详情:${message}`)
Modal.error({
title: '登陆状态已过期,正在退出登陆'
})
store.commit('SYSTEM_CLEAR') // 清除登陆状态
break
case 403:
if (process.env.NODE_ENV === 'development') console.error(`发生错误,错误代码: ${code};详情:${message}`, data)
return Promise.reject(`发生错误!错误代码: ${code},无权限使用此功能`, data)
default:
if (process.env.NODE_ENV === 'development') console.error(`发生错误,错误代码: ${code};详情:${message}`, data)
return Promise.reject(`发生错误!代码: ${code},${message}`, data)
}
}
/**
* 保存登陆数据
* @param {*} jwt token
* @param {*} data 请求数据体
*/
saveLoginInfo(jwt, sessionid, vid, data) {
data = JSON.parse(data)
const { applyCarrier, applyPlatform } = data
store.commit('SYSTEM_LOGIN', { jwt , sessionid, vid, applyCarrier, applyPlatform})
}
}
export default HttpRequest
/** 将全部二级路由转为一级路由 */
export const getAllRouter = route => {
const childRoute = []
route.map(e => childRoute.push(...e.childMenus))
return childRoute
}
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import RouterView from './router-view'
import basisRoute from './modules/basisRoute'
import serviceRoute from './modules/serviceRoute'
const Router = () => {
return (
<BrowserRouter>
<RouterView basisRoute={basisRoute} serviceRoute={serviceRoute} />
</BrowserRouter>
)
}
export default Router
import loadable from '@loadable/component'
const exact = true
export default [
{
url: '/404',
exact,
component: loadable(() => import('@/view/error/404'))
}
]
\ No newline at end of file
export default []
\ No newline at end of file
import React, { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
import { Switch, Route } from 'react-router-dom'
import { APP } from '@/config'
import connect from '../store/modules/route/connect'
import { getAllRouter } from '../libs/utilts'
/** 获取全部路由的路径 */
const getAllRouterName = route => route.map(e => e.url)
const RouterView = ({basisRoute = [], serviceRoute = [], userRoute = [] }) => {
const [openRoute, setOpenRoute] = useState([])
useEffect(() => {
// 二级路由转一级路由
const allRoute = getAllRouter(userRoute)
const allName = getAllRouterName(serviceRoute)
const newRoutes = allRoute.map(e => {
const index = serviceRoute.findIndex(service => service.url === e.url)
const newE = {...e}
if (index !== -1)
newE.component = serviceRoute[index].component
else {
const indexOfName = allName.findIndex(url => url === e.url)
if (indexOfName !== -1)
newE.component = serviceRoute[index].component
else
newE.component = () => {
// location.href = APP.othreDomain + newE.url
return (<>{newE.url}</>)
}
}
return newE
})
setOpenRoute(newRoutes)
}, [userRoute, serviceRoute])
return (
<Switch>
<Layout>
{basisRoute.map(e => <Route path={e.url} exact={e.exact} component={e.component} key={e.url} />)}
{openRoute.map(e => <Route path={e.url} exact={e.exact} component={e.component} key={e.url} />)}
</Layout>
</Switch>
)
}
export default connect(RouterView)
\ No newline at end of file
var STORE_CONTEXT = null
export const createConetxt = store => STORE_CONTEXT = store
export const getStore = () => STORE_CONTEXT
\ No newline at end of file
import { init } from '@rematch/core'
import { createConetxt } from './context'
import route from './modules/route/index'
const store = init({
models: { route }
})
createConetxt(store)
export default store
import { connect } from 'react-redux'
const mapState = state => ({
userRoute: state.route.route,
})
const mapDispatch = dispatch => ({
add: route => dispatch.route.add(route),
change: route => dispatch.route.change(route)
})
export default connect(mapState, mapDispatch)
\ No newline at end of file
import data from './data.json'
export default {
state: {
route: data
},
reducers: {
add(state, route) {
return { route: [...state, route] }
},
change(_, route) {
return { route: [...route] }
}
}
}
import React from 'react'
const Page = ({ history }) => {
return (
<div>
<h1 onClick={() => history.push('/')} >404错误,点击回到首页</h1>
</div>
)
}
export default Page
\ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!