RAP v2.1 commits

test v2.1.0
huoyong.msb 6 years ago
parent 447059f5e5
commit 576b956c5f

@ -5,15 +5,15 @@
"main": "app.js",
"scripts": {
"dev": "npm run start",
"build-css": "node-sass src/ -o src/",
"watch-css": "npm run build-css && node-sass src/ -o src/ --watch --recursive",
"build-css": "node-sass src/ -o build/",
"watch-css": "npm run build-css && node-sass src/ -o build/ --watch --recursive",
"start-js": "react-scripts start",
"start": "npm-run-all -p watch-css start-js",
"build": "npm run build-css && react-scripts build",
"start": "npm-run-all -p watch-css start-js --max-old-space-size=4096",
"build": "npm run lint && npm run build-css && react-scripts build",
"test-backup": "npm run linter && react-scripts test --env=jsdom",
"test": "npm run linter",
"eject": "react-scripts eject",
"linter": "standard --fix"
"lint": "tslint --project ./ -c tslint.json"
},
"repository": {
"type": "git",
@ -22,39 +22,50 @@
"author": "mozhi.gyy@alibaba-inc.com",
"license": "ISC",
"dependencies": {
"animate.css": "3.6.1",
"bootstrap": "^4.1.1",
"chart.js": "^2.6.0",
"codemirror": "5.39.0",
"graceful": "1.0.1",
"jquery": "^3.3.1",
"koa": "2.5.1",
"@material-ui/core": "^4.2.0",
"@material-ui/icons": "^4.2.1",
"@material-ui/pickers": "^3.1.2",
"@material-ui/styles": "^4.2.0",
"animate.css": "3.7.2",
"bootstrap": "^4.3.1",
"chart.js": "^2.8.0",
"classnames": "^2.2.6",
"codemirror": "5.48.0",
"connected-react-router": "^6.5.0",
"debounce-promise": "^3.1.2",
"formik": "^1.5.7",
"formik-material-ui": "^0.0.19",
"graceful": "1.0.2",
"history": "^4.9.0",
"jquery": "^3.4.1",
"koa": "2.7.0",
"koa-router": "7.4.0",
"koa-session": "5.8.1",
"koa-session": "5.12.2",
"koa-static": "5.0.0",
"lodash": "4.17.10",
"lodash": "4.17.13",
"mockjs": "1.0.1-beta3",
"moment": "2.22.2",
"node-fetch": "2.1.2",
"normalizr": "^3.2.4",
"moment": "2.24.0",
"node-fetch": "2.6.0",
"normalizr": "^3.4.0",
"nprogress": "0.2.0",
"parsleyjs": "^2.7.2",
"popper.js": "^1.14.3",
"prop-types": "15.6.2",
"react": "16.4.1",
"react-dom": "16.4.1",
"react-icons": "2.2.7",
"react-modal": "3.4.5",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-config": "1.0.0-beta.4",
"react-router-dom": "4.3.1",
"react-router-redux": "5.0.0-alpha.6",
"redux": "4.0.0",
"redux-saga": "0.16.0",
"reselect": "^3.0.1",
"sortablejs": "1.7.0",
"urijs": "1.19.1"
"parsleyjs": "^2.9.1",
"popper.js": "^1.15.0",
"prop-types": "15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-icons": "3.7.0",
"react-modal": "3.8.2",
"react-redux": "^7.1.0",
"react-router": "5.0.1",
"react-router-config": "5.0.1",
"react-router-dom": "5.0.1",
"react-select": "^3.0.4",
"redux": "4.0.3",
"redux-saga": "1.0.5",
"reselect": "^4.0.0",
"sortablejs": "1.9.0",
"urijs": "1.19.1",
"yup": "^0.27.0"
},
"standard": {
"parser": "babel-eslint",
@ -66,10 +77,49 @@
]
},
"devDependencies": {
"babel-eslint": "7.2.3",
"node-sass": "4.9.0",
"npm-run-all": "4.1.3",
"react-scripts": "1.1.4",
"standard": "11.0.1"
"@types/chart.js": "^2.7.55",
"@types/classnames": "^2.2.9",
"@types/codemirror": "^0.0.76",
"@types/history": "^4.7.2",
"@types/jest": "^24.0.15",
"@types/jquery": "^3.3.30",
"@types/lodash": "^4.14.136",
"@types/mockjs": "^1.0.2",
"@types/node": "^12.6.2",
"@types/nprogress": "^0.2.0",
"@types/react": "^16.8.23",
"@types/react-dom": "^16.8.4",
"@types/react-modal": "^3.8.2",
"@types/react-redux": "^7.1.1",
"@types/react-router-config": "^5.0.0",
"@types/react-router-dom": "^4.3.4",
"@types/react-select": "^3.0.0",
"@types/sortablejs": "^1.7.2",
"@types/urijs": "^1.19.3",
"@types/yup": "^0.26.21",
"node-sass": "4.12.0",
"npm-run-all": "4.1.5",
"pre-commit": "^1.2.2",
"react-scripts": "^3.0.1",
"standard": "12.0.1",
"tslint": "^5.18.0",
"tslint-react": "^4.0.0",
"tslint-react-hooks": "^2.1.1",
"typescript": "^3.5.3"
},
"pre-commit": [
"lint"
],
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

@ -1,67 +0,0 @@
// 登陆
export const login = (user, onResolved) => ({ type: 'USER_LOGIN', user, onResolved })
export const loginSucceeded = (user) => ({ type: 'USER_LOGIN_SUCCEEDED', user })
export const loginFailed = (message) => ({ type: 'USER_LOGIN_FAILED', message })
// 重置
export const reset = (email, password, onResolved) => ({ type: 'USER_RESET', email, password, onResolved })
export const resetSucceeded = () => ({ type: 'USER_RESET_SUCCEEDED' })
export const resetFailed = (message) => ({ type: 'USER_RESET_FAILED', message })
// 登出
export const logout = () => ({ type: 'USER_LOGOUT' })
export const logoutSucceeded = () => ({ type: 'USER_LOGOUT_SUCCEEDED' })
export const logoutFailed = () => ({ type: 'USER_LOGOUT_FAILED' })
// 获取登陆信息
export const fetchLoginInfo = () => ({ type: 'USER_FETCH_LOGIN_INFO' })
export const fetchLoginInfoSucceeded = (user) => ({ type: 'USER_FETCH_LOGIN_INFO_SUCCEEDED', user })
export const fetchLoginInfoFailed = (message) => ({ type: 'USER_FETCH_LOGIN_INFO_FAILED', message })
// 注册
export const addUser = (user, onResolved) => ({ type: 'USER_ADD', user, onResolved })
export const addUserSucceeded = (user) => ({ type: 'USER_ADD_SUCCEEDED', user })
export const addUserFailed = (message) => ({ type: 'USER_ADD_FAILED', message })
// 更新
export const updateUser = (user, onResolved) => ({ type: 'USER_UPDATE', user, onResolved })
export const updateUserSucceeded = (user) => ({ type: 'USER_UPDATE_SUCCEEDED', user })
export const updateUserFailed = (message) => ({ type: 'USER_UPDATE_FAILED', message })
// 删除用户
export const deleteUser = (id) => ({ type: 'USER_DELETE', id })
export const deleteUserSucceeded = (id) => ({ type: 'USER_DELETE_SUCCEEDED', id })
export const deleteUserFailed = (id) => ({ type: 'USER_DELETE_FAILED', id })
// 获取用户列表
export const fetchUserCount = () => ({ type: 'USER_COUNT_FETCH' })
export const fetchUserCountSucceeded = (count) => ({ type: 'USER_COUNT_FETCH_SUCCEEDED', count })
export const fetchUserCountFailed = (message) => ({ type: 'USER_COUNT_FETCH_FAILED', message })
export const fetchUserList = ({ cursor, limit } = {}) => ({ type: 'USER_LIST_FETCH', cursor, limit })
export const fetchUserListSucceeded = (users) => ({ type: 'USER_LIST_FETCH_SUCCEEDED', users })
export const fetchUserListFailed = (message) => ({ type: 'USER_LIST_FETCH_FAILED', message })
// 获取用户设置
export const fetchSetting = () => ({ type: 'SETTING_FETCH' })
export const fetchSettingSucceeded = (setting) => ({ type: 'SETTING_FETCH_SUCCEEDED', setting })
export const fetchSettingFailed = (message) => ({ type: 'SETTING_FETCH_FAILED', message })
// 修改用户设置
export const updateSetting = (setting) => ({ type: 'SETTING_UPDATE', setting })
export const updateSettingSucceeded = (setting) => ({ type: 'SETTING_UPDATE', setting })
export const updateSettingFailed = (message) => ({ type: 'SETTING_UPDATE', message })
// 获取用户通知
export const fetchNotificationList = () => ({ type: 'NOTIFICATION_LIST_FETCH' })
export const fetchNotificationListSucceeded = () => ({ type: 'NOTIFICATION_LIST_FETCH_SUCCEEDED' })
export const fetchNotificationListFailed = () => ({ type: 'NOTIFICATION_LIST_FETCH_Failed' })
// 阅读用户通知
export const readNotification = (id) => ({ type: 'NOTIFICATION_READ', id })
export const readNotificationSucceeded = (id) => ({ type: 'NOTIFICATION_READ_SUCCEEDED', id })
export const readNotificationFailed = (message) => ({ type: 'NOTIFICATION_READ_FAILED', message })
// 获取用户日志
export const fetchLogList = ({ cursor, limit } = {}) => ({ type: 'LOG_LIST_FETCH', cursor, limit })
export const fetchLogListSucceeded = (logs) => ({ type: 'LOG_LIST_FETCH_SUCCEEDED', logs })
export const fetchLogListFailed = (message) => ({ type: 'LOG_LIST_FETCH_FAILED', message })

@ -0,0 +1,59 @@
// 登陆
export const login = (user: any, onResolved: any) => ({ type: 'USER_LOGIN', user, onResolved })
export const loginSucceeded = (user: any) => ({ type: 'USER_LOGIN_SUCCEEDED', user })
export const loginFailed = (message: any) => ({ type: 'USER_LOGIN_FAILED', message })
// 登出
export const logout = () => ({ type: 'USER_LOGOUT' })
export const logoutSucceeded = () => { return ({ type: 'USER_LOGOUT_SUCCEEDED' }) }
export const logoutFailed = () => ({ type: 'USER_LOGOUT_FAILED' })
// 获取登陆信息
export const fetchLoginInfo = () => ({ type: 'USER_FETCH_LOGIN_INFO' })
export const fetchLoginInfoSucceeded = (user: any) => ({ type: 'USER_FETCH_LOGIN_INFO_SUCCEEDED', user })
export const fetchLoginInfoFailed = (message: any) => ({ type: 'USER_FETCH_LOGIN_INFO_FAILED', message })
// 注册
export const addUser = (user: any, onResolved: any) => ({ type: 'USER_ADD', user, onResolved })
export const addUserSucceeded = (user: any) => ({ type: 'USER_ADD_SUCCEEDED', user })
export const addUserFailed = (message: any) => ({ type: 'USER_ADD_FAILED', message })
// 删除用户
export const deleteUser = (id: any) => ({ type: 'USER_DELETE', id })
export const deleteUserSucceeded = (id: any) => ({ type: 'USER_DELETE_SUCCEEDED', id })
export const deleteUserFailed = (id: any) => ({ type: 'USER_DELETE_FAILED', id })
// 获取用户列表
export const fetchUserCount = () => ({ type: 'USER_COUNT_FETCH' })
export const fetchUserCountSucceeded = (count: any) => ({ type: 'USER_COUNT_FETCH_SUCCEEDED', count })
export const fetchUserCountFailed = (message: any) => ({ type: 'USER_COUNT_FETCH_FAILED', message })
export const fetchUserList = ({ cursor, limit } = { cursor: '', limit: ''}) => ({ type: 'USER_LIST_FETCH', cursor, limit })
export const fetchUserListSucceeded = (users: any) => ({ type: 'USER_LIST_FETCH_SUCCEEDED', users })
export const fetchUserListFailed = (message: any) => ({ type: 'USER_LIST_FETCH_FAILED', message })
// 获取用户设置
export const fetchSetting = () => ({ type: 'SETTING_FETCH' })
export const fetchSettingSucceeded = (setting: any) => ({ type: 'SETTING_FETCH_SUCCEEDED', setting })
export const fetchSettingFailed = (message: any) => ({ type: 'SETTING_FETCH_FAILED', message })
// 修改用户设置
export const updateSetting = (setting: any) => ({ type: 'SETTING_UPDATE', setting })
export const updateSettingSucceeded = (setting: any) => ({ type: 'SETTING_UPDATE', setting })
export const updateSettingFailed = (message: any) => ({ type: 'SETTING_UPDATE', message })
// 获取用户通知
export const fetchNotificationList = () => ({ type: 'NOTIFICATION_LIST_FETCH' })
export const fetchNotificationListSucceeded = () => ({ type: 'NOTIFICATION_LIST_FETCH_SUCCEEDED' })
export const fetchNotificationListFailed = () => ({ type: 'NOTIFICATION_LIST_FETCH_Failed' })
// 阅读用户通知
export const readNotification = (id: any) => ({ type: 'NOTIFICATION_READ', id })
export const readNotificationSucceeded = (id: any) => ({ type: 'NOTIFICATION_READ_SUCCEEDED', id })
export const readNotificationFailed = (message: any) => ({ type: 'NOTIFICATION_READ_FAILED', message })
// 获取用户日志
export const fetchLogList = (
{ cursor, limit } = { cursor: '', limit: '' }
) => ({ type: 'LOG_LIST_FETCH', cursor, limit })
export const fetchLogListSucceeded = (logs: any) => ({ type: 'LOG_LIST_FETCH_SUCCEEDED', logs })
export const fetchLogListFailed = (message: any) => ({ type: 'LOG_LIST_FETCH_FAILED', message })

@ -1,20 +0,0 @@
// 获取平台计数信息
export const fetchCounter = () => ({ type: 'ANALYTICS_COUNTER_FETCH' })
export const fetchCounterSucceeded = (counter) => ({ type: 'ANALYTICS_COUNTER_FETCH_SUCCEEDED', counter })
export const fetchCounterFailed = (message) => ({ type: 'ANALYTICS_COUNTER_FETCH_FAILED', message })
export const fetchAnalyticsRepositoriesCreated = ({ start, end } = {}) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED', start, end })
export const fetchAnalyticsRepositoriesCreatedSucceeded = (analytics) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesCreatedFailed = (message) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED_FAILED', message })
export const fetchAnalyticsRepositoriesUpdated = ({ start, end } = {}) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED', start, end })
export const fetchAnalyticsRepositoriesUpdatedSucceeded = (analytics) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesUpdatedFailed = (message) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED_FAILED', message })
export const fetchAnalyticsUsersActivation = ({ start, end } = {}) => ({ type: 'ANALYTICS_USERS_ACTIVATION', start, end })
export const fetchAnalyticsUsersActivationSucceeded = (analytics) => ({ type: 'ANALYTICS_USERS_ACTIVATION_SUCCEEDED', analytics })
export const fetchAnalyticsUsersActivationFailed = (message) => ({ type: 'ANALYTICS_USERS_ACTIVATION_FAILED', message })
export const fetchAnalyticsRepositoriesActivation = ({ start, end } = {}) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION', start, end })
export const fetchAnalyticsRepositoriesActivationSucceeded = (analytics) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesActivationFailed = (message) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION_FAILED', message })

@ -0,0 +1,28 @@
// 获取平台计数信息
export const fetchCounter = () => ({ type: 'ANALYTICS_COUNTER_FETCH' })
export const fetchCounterSucceeded = (counter: any) => ({ type: 'ANALYTICS_COUNTER_FETCH_SUCCEEDED', counter })
export const fetchCounterFailed = (message: any) => ({ type: 'ANALYTICS_COUNTER_FETCH_FAILED', message })
export const fetchAnalyticsRepositoriesCreated = (
{ start, end } = { start: '', end: '' }
) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED', start, end })
export const fetchAnalyticsRepositoriesCreatedSucceeded = (analytics: any) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesCreatedFailed = (message: any) => ({ type: 'ANALYTICS_REPOSITORIES_CREATED_FAILED', message })
export const fetchAnalyticsRepositoriesUpdated = (
{ start, end } = { start: '', end: '' }
) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED', start, end })
export const fetchAnalyticsRepositoriesUpdatedSucceeded = (analytics: any) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesUpdatedFailed = (message: any) => ({ type: 'ANALYTICS_REPOSITORIES_UPDATED_FAILED', message })
export const fetchAnalyticsUsersActivation = (
{ start, end } = { start: '', end: '' }
) => ({ type: 'ANALYTICS_USERS_ACTIVATION', start, end })
export const fetchAnalyticsUsersActivationSucceeded = (analytics: any) => ({ type: 'ANALYTICS_USERS_ACTIVATION_SUCCEEDED', analytics })
export const fetchAnalyticsUsersActivationFailed = (message: any) => ({ type: 'ANALYTICS_USERS_ACTIVATION_FAILED', message })
export const fetchAnalyticsRepositoriesActivation = (
{ start, end } = { start: '', end: '' }
) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION', start, end })
export const fetchAnalyticsRepositoriesActivationSucceeded = (analytics: any) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION_SUCCEEDED', analytics })
export const fetchAnalyticsRepositoriesActivationFailed = (message: any) => ({ type: 'ANALYTICS_REPOSITORIES_ACTIVATION_FAILED', message })

@ -0,0 +1 @@
export const refresh = () => ({ type: 'REFRESH' })

@ -1,113 +0,0 @@
export const addInterface = (itf, onResolved) => ({
type: 'INTERFACE_ADD',
interface: itf,
onResolved
})
export const addInterfaceSucceeded = (payload) => ({
type: 'INTERFACE_ADD_SUCCEEDED',
payload
})
export const addInterfaceFailed = (message) => ({
type: 'INTERFACE_ADD_FAILED',
message
})
export const updateInterface = (itf, onResolved) => ({
type: 'INTERFACE_UPDATE',
interface: itf,
onResolved
})
export const updateInterfaceSucceeded = (payload) => ({
type: 'INTERFACE_UPDATE_SUCCEEDED',
payload
})
export const updateInterfaceFailed = (message) => ({
type: 'INTERFACE_UPDATE_FAILED',
message
})
export const moveInterface = (params, onResolved) => ({
type: 'INTERFACE_MOVE',
params,
onResolved
})
export const moveInterfaceSucceeded = () => ({
type: 'INTERFACE_MOVE_SUCCEEDED'
})
export const moveInterfaceFailed = (message) => ({
type: 'INTERFACE_MOVE_FAILED',
message
})
export const deleteInterface = (id, onResolved) => ({
type: 'INTERFACE_DELETE',
id,
onResolved
})
export const deleteInterfaceSucceeded = (payload) => ({
type: 'INTERFACE_DELETE_SUCCEEDED',
payload
})
export const deleteInterfaceFailed = (message) => ({
type: 'INTERFACE_DELETE_FAILED',
message
})
export const fetchInterfaceCount = () => ({
type: 'INTERFACE_COUNT_FETCH'
})
export const fetchInterfaceCountSucceeded = (count) => ({
type: 'INTERFACE_COUNT_FETCH_SUCCEEDED',
count
})
export const fetchInterfaceCountFailed = (message) => ({
type: 'INTERFACE_COUNT_FETCH_FAILED',
message
})
export const lockInterface = (id, onResolved) => ({
type: 'INTERFACE_LOCK',
id,
onResolved
})
export const lockInterfaceSucceeded = (itfId, locker) => ({
type: 'INTERFACE_LOCK_SUCCEEDED',
payload: {
itfId,
locker
}
})
export const lockInterfaceFailed = (message) => ({
type: 'INTERFACE_LOCK_FAILED',
message
})
export const unlockInterface = (id, onResolved) => ({
type: 'INTERFACE_UNLOCK',
id,
onResolved
})
export const unlockInterfaceSucceeded = (itfId) => ({
type: 'INTERFACE_UNLOCK_SUCCEEDED',
payload: {
itfId
},
})
export const unlockInterfaceFailed = (message) => ({
type: 'INTERFACE_UNLOCK_FAILED',
message
})
export const sortInterfaceList = (ids, onResolved) => ({
type: 'INTERFACE_LIST_SORT',
ids,
onResolved
})
export const sortInterfaceListSucceeded = (count) => ({
type: 'INTERFACE_LIST_SORT_SUCCEEDED',
count
})
export const sortInterfaceListFailed = (message) => ({
type: 'INTERFACE_LIST_SORT_FAILED',
message
})

@ -0,0 +1,113 @@
export const addInterface = (itf: any, onResolved: any) => ({
type: 'INTERFACE_ADD',
interface: itf,
onResolved,
})
export const addInterfaceSucceeded = (payload: any) => ({
type: 'INTERFACE_ADD_SUCCEEDED',
payload,
})
export const addInterfaceFailed = (message: any) => ({
type: 'INTERFACE_ADD_FAILED',
message,
})
export const updateInterface = (itf: any, onResolved: any) => ({
type: 'INTERFACE_UPDATE',
interface: itf,
onResolved,
})
export const updateInterfaceSucceeded = (payload: any) => ({
type: 'INTERFACE_UPDATE_SUCCEEDED',
payload,
})
export const updateInterfaceFailed = (message: any) => ({
type: 'INTERFACE_UPDATE_FAILED',
message,
})
export const moveInterface = (params: any, onResolved: any) => ({
type: 'INTERFACE_MOVE',
params,
onResolved,
})
export const moveInterfaceSucceeded = () => ({
type: 'INTERFACE_MOVE_SUCCEEDED',
})
export const moveInterfaceFailed = (message: any) => ({
type: 'INTERFACE_MOVE_FAILED',
message,
})
export const deleteInterface = (id: any, onResolved: any) => ({
type: 'INTERFACE_DELETE',
id,
onResolved,
})
export const deleteInterfaceSucceeded = (payload: any) => ({
type: 'INTERFACE_DELETE_SUCCEEDED',
payload,
})
export const deleteInterfaceFailed = (message: any) => ({
type: 'INTERFACE_DELETE_FAILED',
message,
})
export const fetchInterfaceCount = () => ({
type: 'INTERFACE_COUNT_FETCH',
})
export const fetchInterfaceCountSucceeded = (count: any) => ({
type: 'INTERFACE_COUNT_FETCH_SUCCEEDED',
count,
})
export const fetchInterfaceCountFailed = (message: any) => ({
type: 'INTERFACE_COUNT_FETCH_FAILED',
message,
})
export const lockInterface = (id: any, onResolved?: any) => ({
type: 'INTERFACE_LOCK',
id,
onResolved,
})
export const lockInterfaceSucceeded = (itfId: any, locker: any) => ({
type: 'INTERFACE_LOCK_SUCCEEDED',
payload: {
itfId,
locker,
},
})
export const lockInterfaceFailed = (message: any) => ({
type: 'INTERFACE_LOCK_FAILED',
message,
})
export const unlockInterface = (id: any, onResolved?: any) => ({
type: 'INTERFACE_UNLOCK',
id,
onResolved,
})
export const unlockInterfaceSucceeded = (itfId: any) => ({
type: 'INTERFACE_UNLOCK_SUCCEEDED',
payload: {
itfId,
},
})
export const unlockInterfaceFailed = (message: any) => ({
type: 'INTERFACE_UNLOCK_FAILED',
message,
})
export const sortInterfaceList = (ids: any, onResolved: any) => ({
type: 'INTERFACE_LIST_SORT',
ids,
onResolved,
})
export const sortInterfaceListSucceeded = (count: any) => ({
type: 'INTERFACE_LIST_SORT_SUCCEEDED',
count,
})
export const sortInterfaceListFailed = (message: any) => ({
type: 'INTERFACE_LIST_SORT_FAILED',
message,
})

@ -1,56 +0,0 @@
export const addModule = (module, onResolved) => ({
type: 'MODULE_ADD',
module,
onResolved
})
export const addModuleSucceeded = (module) => ({
type: 'MODULE_ADD_SUCCEEDED',
module
})
export const addModuleFailed = (message) => ({
type: 'MODULE_ADD_FAILED',
message
})
export const updateModule = (module, onResolved) => ({
type: 'MODULE_UPDATE',
module,
onResolved
})
export const updateModuleSucceeded = (payload) => ({
type: 'MODULE_UPDATE_SUCCEEDED',
payload
})
export const updateModuleFailed = (message) => ({
type: 'MODULE_UPDATE_FAILED',
message
})
export const deleteModule = (id, onResolved, repoId) => ({
type: 'MODULE_DELETE',
id,
onResolved,
repoId
})
export const deleteModuleSucceeded = (id) => ({
type: 'MODULE_DELETE_SUCCEEDED',
id
})
export const deleteModuleFailed = (message) => ({
type: 'MODULE_DELETE_FAILED',
message
})
export const sortModuleList = (ids, onResolved) => ({
type: 'MODULE_LIST_SORT',
ids,
onResolved
})
export const sortModuleListSucceeded = (count) => ({
type: 'MODULE_LIST_SORT_SUCCEEDED',
count
})
export const sortModuleListFailed = (message) => ({
type: 'MODULE_LIST_SORT_FAILED',
message
})

@ -0,0 +1,56 @@
export const addModule = (module: any, onResolved: any) => ({
type: 'MODULE_ADD',
module,
onResolved,
})
export const addModuleSucceeded = (module: any) => ({
type: 'MODULE_ADD_SUCCEEDED',
module,
})
export const addModuleFailed = (message: any) => ({
type: 'MODULE_ADD_FAILED',
message,
})
export const updateModule = (module: any, onResolved: any) => ({
type: 'MODULE_UPDATE',
module,
onResolved,
})
export const updateModuleSucceeded = (payload: any) => ({
type: 'MODULE_UPDATE_SUCCEEDED',
payload,
})
export const updateModuleFailed = (message: any) => ({
type: 'MODULE_UPDATE_FAILED',
message,
})
export const deleteModule = (id: any, onResolved: any, repoId: any) => ({
type: 'MODULE_DELETE',
id,
onResolved,
repoId,
})
export const deleteModuleSucceeded = (id: any) => ({
type: 'MODULE_DELETE_SUCCEEDED',
id,
})
export const deleteModuleFailed = (message: any) => ({
type: 'MODULE_DELETE_FAILED',
message,
})
export const sortModuleList = (ids: any, onResolved: any) => ({
type: 'MODULE_LIST_SORT',
ids,
onResolved,
})
export const sortModuleListSucceeded = (count: any) => ({
type: 'MODULE_LIST_SORT_SUCCEEDED',
count,
})
export const sortModuleListFailed = (message: any) => ({
type: 'MODULE_LIST_SORT_FAILED',
message,
})

@ -1,31 +0,0 @@
export const addOrganization = (organization, onResolved) => ({ type: 'ORGANIZATION_ADD', organization, onResolved })
export const addOrganizationSucceeded = (organization) => ({ type: 'ORGANIZATION_ADD_SUCCEEDED', organization })
export const addOrganizationFailed = (message) => ({ type: 'ORGANIZATION_ADD_FAILED', message })
export const updateOrganization = (organization, onResolved) => ({ type: 'ORGANIZATION_UPDATE', organization, onResolved })
export const updateOrganizationSucceeded = (organization) => ({ type: 'ORGANIZATION_UPDATE_SUCCEEDED', organization })
export const updateOrganizationFailed = (message) => ({ type: 'ORGANIZATION_UPDATE_FAILED', message })
export const deleteOrganization = (id, onResolved) => ({ type: 'ORGANIZATION_DELETE', id, onResolved })
export const deleteOrganizationSucceeded = (id) => ({ type: 'ORGANIZATION_DELETE_SUCCEEDED', id })
export const deleteOrganizationFailed = (message) => ({ type: 'ORGANIZATION_DELETE_FAILED', message })
export const fetchOrganization = ({ id, organization } = {}) => ({ type: 'ORGANIZATION_FETCH', id, organization })
export const fetchOrganizationSucceeded = (organization) => ({ type: 'ORGANIZATION_FETCH_SUCCEEDED', organization })
export const fetchOrganizationFailed = (message) => ({ type: 'ORGANIZATION_FETCH_FAILED', message })
export const fetchOrganizationCount = () => ({ type: 'ORGANIZATION_COUNT_FETCH' })
export const fetchOrganizationCountSucceeded = (count) => ({ type: 'ORGANIZATION_COUNT_FETCH_SUCCEEDED', count })
export const fetchOrganizationCountFailed = (message) => ({ type: 'ORGANIZATION_COUNT_FETCH_FAILED', message })
export const fetchOwnedOrganizationList = ({ name } = {}) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH', name })
export const fetchOwnedOrganizationListSucceeded = (organizations) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchOwnedOrganizationListFailed = (message) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH_FAILED', message })
export const fetchJoinedOrganizationList = ({ name } = {}) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH', name })
export const fetchJoinedOrganizationListSucceeded = (organizations) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchJoinedOrganizationListFailed = (message) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH_FAILED', message })
export const fetchOrganizationList = ({ name, cursor, limit } = {}) => ({ type: 'ORGANIZATION_LIST_FETCH', name, cursor, limit })
export const fetchOrganizationListSucceeded = (organizations) => ({ type: 'ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchOrganizationListFailed = (message) => ({ type: 'ORGANIZATION_LIST_FETCH_FAILED', message })

@ -0,0 +1,33 @@
export const addOrganization = (organization: any, onResolved: any) => ({ type: 'ORGANIZATION_ADD', organization, onResolved })
export const addOrganizationSucceeded = (organization: any) => ({ type: 'ORGANIZATION_ADD_SUCCEEDED', organization })
export const addOrganizationFailed = (message: any) => ({ type: 'ORGANIZATION_ADD_FAILED', message })
export const updateOrganization = (organization: any, onResolved: any) => ({ type: 'ORGANIZATION_UPDATE', organization, onResolved })
export const updateOrganizationSucceeded = (organization: any) => ({ type: 'ORGANIZATION_UPDATE_SUCCEEDED', organization })
export const updateOrganizationFailed = (message: any) => ({ type: 'ORGANIZATION_UPDATE_FAILED', message })
export const deleteOrganization = (id: any, onResolved: any) => ({ type: 'ORGANIZATION_DELETE', id, onResolved })
export const deleteOrganizationSucceeded = (id: any) => ({ type: 'ORGANIZATION_DELETE_SUCCEEDED', id })
export const deleteOrganizationFailed = (message: any) => ({ type: 'ORGANIZATION_DELETE_FAILED', message })
export const fetchOrganization = ({ id, organization } = { id: '', organization: ''}) => ({ type: 'ORGANIZATION_FETCH', id, organization })
export const fetchOrganizationSucceeded = (organization: any) => ({ type: 'ORGANIZATION_FETCH_SUCCEEDED', organization })
export const fetchOrganizationFailed = (message: any) => ({ type: 'ORGANIZATION_FETCH_FAILED', message })
export const fetchOrganizationCount = () => ({ type: 'ORGANIZATION_COUNT_FETCH' })
export const fetchOrganizationCountSucceeded = (count: any) => ({ type: 'ORGANIZATION_COUNT_FETCH_SUCCEEDED', count })
export const fetchOrganizationCountFailed = (message: any) => ({ type: 'ORGANIZATION_COUNT_FETCH_FAILED', message })
export const fetchOwnedOrganizationList = ({ name } = { name: ''}) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH', name })
export const fetchOwnedOrganizationListSucceeded = (organizations: any) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchOwnedOrganizationListFailed = (message: any) => ({ type: 'OWNED_ORGANIZATION_LIST_FETCH_FAILED', message })
export const fetchJoinedOrganizationList = ({ name } = { name: ''}) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH', name })
export const fetchJoinedOrganizationListSucceeded = (organizations: any) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchJoinedOrganizationListFailed = (message: any) => ({ type: 'JOINED_ORGANIZATION_LIST_FETCH_FAILED', message })
export const fetchOrganizationList = (
{ name, cursor, limit } = { name: '', cursor: '', limit: '' }
) => ({ type: 'ORGANIZATION_LIST_FETCH', name, cursor, limit })
export const fetchOrganizationListSucceeded = (organizations: any) => ({ type: 'ORGANIZATION_LIST_FETCH_SUCCEEDED', organizations })
export const fetchOrganizationListFailed = (message: any) => ({ type: 'ORGANIZATION_LIST_FETCH_FAILED', message })

@ -1,71 +0,0 @@
export const addProperty = (property, onResolved) => ({
type: 'PROPERTY_ADD',
property,
onResolved
})
export const addPropertySucceeded = (property) => ({
type: 'PROPERTY_ADD_SUCCEEDED',
property
})
export const addPropertyFailed = (message) => ({
type: 'PROPERTY_ADD_FAILED',
message
})
export const updateProperty = (property, onResolved) => ({
type: 'PROPERTY_UPDATE',
property,
onResolved
})
export const updatePropertySucceeded = (property) => ({
type: 'PROPERTY_UPDATE_SUCCEEDED',
property
})
export const updatePropertyFailed = (message) => ({
type: 'PROPERTY_UPDATE_FAILED',
message
})
export const updateProperties = (itf, properties, summary, onResolved) => ({
type: 'PROPERTIES_UPDATE',
itf,
properties,
summary,
onResolved
})
export const updatePropertiesSucceeded = (payload) => ({
type: 'PROPERTIES_UPDATE_SUCCEEDED',
payload
})
export const updatePropertiesFailed = (message) => ({
type: 'PROPERTIES_UPDATE_FAILED',
message
})
export const deleteProperty = (id, onResolved) => ({
type: 'PROPERTY_DELETE',
id,
onResolved
})
export const deletePropertySucceeded = (id) => ({
type: 'PROPERTY_DELETE_SUCCEEDED',
id
})
export const deletePropertyFailed = (message) => ({
type: 'PROPERTY_DELETE_FAILED',
message
})
export const sortPropertyList = (ids, onResolved) => ({
type: 'PROPERTY_LIST_SORT',
ids,
onResolved
})
export const sortPropertyListSucceeded = (count) => ({
type: 'PROPERTY_LIST_SORT_SUCCEEDED',
count
})
export const sortPropertyListFailed = (message) => ({
type: 'PROPERTY_LIST_SORT_FAILED',
message
})

@ -0,0 +1,71 @@
export const addProperty = (property: any, onResolved: any) => ({
type: 'PROPERTY_ADD',
property,
onResolved,
})
export const addPropertySucceeded = (property: any) => ({
type: 'PROPERTY_ADD_SUCCEEDED',
property,
})
export const addPropertyFailed = (message: any) => ({
type: 'PROPERTY_ADD_FAILED',
message,
})
export const updateProperty = (property: any, onResolved: any) => ({
type: 'PROPERTY_UPDATE',
property,
onResolved,
})
export const updatePropertySucceeded = (property: any) => ({
type: 'PROPERTY_UPDATE_SUCCEEDED',
property,
})
export const updatePropertyFailed = (message: any) => ({
type: 'PROPERTY_UPDATE_FAILED',
message,
})
export const updateProperties = (itf: any, properties: any, summary: any, onResolved: any) => ({
type: 'PROPERTIES_UPDATE',
itf,
properties,
summary,
onResolved,
})
export const updatePropertiesSucceeded = (payload: any) => ({
type: 'PROPERTIES_UPDATE_SUCCEEDED',
payload,
})
export const updatePropertiesFailed = (message: any) => ({
type: 'PROPERTIES_UPDATE_FAILED',
message,
})
export const deleteProperty = (id: any, onResolved: any) => ({
type: 'PROPERTY_DELETE',
id,
onResolved,
})
export const deletePropertySucceeded = (id: any) => ({
type: 'PROPERTY_DELETE_SUCCEEDED',
id,
})
export const deletePropertyFailed = (message: any) => ({
type: 'PROPERTY_DELETE_FAILED',
message,
})
export const sortPropertyList = (ids: any, onResolved: any) => ({
type: 'PROPERTY_LIST_SORT',
ids,
onResolved,
})
export const sortPropertyListSucceeded = (count: any) => ({
type: 'PROPERTY_LIST_SORT_SUCCEEDED',
count,
})
export const sortPropertyListFailed = (message: any) => ({
type: 'PROPERTY_LIST_SORT_FAILED',
message,
})

@ -1,37 +0,0 @@
export const addRepository = (repository, onResolved) => ({ type: 'REPOSITORY_ADD', repository, onResolved })
export const addRepositorySucceeded = (repository) => ({ type: 'REPOSITORY_ADD_SUCCEEDED', repository })
export const addRepositoryFailed = (message) => ({ type: 'REPOSITORY_ADD_FAILED', message })
export const importRepository = (data, onResolved) => ({ type: 'REPOSITORY_IMPORT', onResolved, data })
export const importRepositorySucceeded = () => ({ type: 'REPOSITORY_IMPORT_SUCCEEDED' })
export const importRepositoryFailed = (message) => ({ type: 'REPOSITORY_IMPORT_FAILED', message })
export const updateRepository = (repository, onResolved) => ({ type: 'REPOSITORY_UPDATE', repository, onResolved })
export const updateRepositorySucceeded = (repository) => ({ type: 'REPOSITORY_UPDATE_SUCCEEDED', repository })
export const updateRepositoryFailed = (message) => ({ type: 'REPOSITORY_UPDATE_FAILED', message })
export const deleteRepository = (id) => ({ type: 'REPOSITORY_DELETE', id })
export const deleteRepositorySucceeded = (id) => ({ type: 'REPOSITORY_DELETE_SUCCEEDED', id })
export const deleteRepositoryFailed = (message) => ({ type: 'REPOSITORY_DELETE_FAILED', message })
export const fetchRepository = ({ id, repository } = {}) => ({ type: 'REPOSITORY_FETCH', id, repository })
export const fetchRepositorySucceeded = (repository) => ({ type: 'REPOSITORY_FETCH_SUCCEEDED', repository })
export const fetchRepositoryFailed = (message) => ({ type: 'REPOSITORY_FETCH_FAILED', message })
export const clearRepository = () => ({ type: 'REPOSITORY_CLEAR' })
export const fetchRepositoryCount = () => ({ type: 'REPOSITORY_COUNT_FETCH' })
export const fetchRepositoryCountSucceeded = (count) => ({ type: 'REPOSITORY_COUNT_FETCH_SUCCEEDED', count })
export const fetchRepositoryCountFailed = (message) => ({ type: 'REPOSITORY_COUNT_FETCH_FAILED', message })
export const fetchRepositoryList = ({ user, organization, name, cursor, limit } = {}) => ({ type: 'REPOSITORY_LIST_FETCH', user, organization, name, cursor, limit })
export const fetchRepositoryListSucceeded = (repositories) => ({ type: 'REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchRepositoryListFailed = (message) => ({ type: 'REPOSITORY_LIST_FETCH_FAILED', message })
export const fetchOwnedRepositoryList = ({ user, name } = {}) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH', user, name })
export const fetchOwnedRepositoryListSucceeded = (repositories) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchOwnedRepositoryListFailed = (message) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH_FAILED', message })
export const fetchJoinedRepositoryList = ({ user, name } = {}) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH', user, name })
export const fetchJoinedRepositoryListSucceeded = (repositories) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchJoinedRepositoryListFailed = (message) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH_FAILED', message })

@ -0,0 +1,48 @@
export const addRepository = (repository: any, onResolved: any) => ({ type: 'REPOSITORY_ADD', repository, onResolved })
export const addRepositorySucceeded = (repository: any) => ({ type: 'REPOSITORY_ADD_SUCCEEDED', repository })
export const addRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_ADD_FAILED', message })
export const importRepository = (data: any, onResolved: any) => ({ type: 'REPOSITORY_IMPORT', onResolved, data })
export const importRepositorySucceeded = () => ({ type: 'REPOSITORY_IMPORT_SUCCEEDED' })
export const importRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_IMPORT_FAILED', message })
export const updateRepository = (repository: any, onResolved: any) => ({ type: 'REPOSITORY_UPDATE', repository, onResolved })
export const updateRepositorySucceeded = (repository: any) => ({ type: 'REPOSITORY_UPDATE_SUCCEEDED', repository })
export const updateRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_UPDATE_FAILED', message })
export const deleteRepository = (id: any) => ({ type: 'REPOSITORY_DELETE', id })
export const deleteRepositorySucceeded = (id: any) => ({ type: 'REPOSITORY_DELETE_SUCCEEDED', id })
export const deleteRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_DELETE_FAILED', message })
export const fetchRepository = ({ id, repository }: any) => ({ type: 'REPOSITORY_FETCH', id, repository })
export const fetchRepositorySucceeded = (repository: any) => ({ type: 'REPOSITORY_FETCH_SUCCEEDED', repository })
export const fetchRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_FETCH_FAILED', message })
export const clearRepository = () => ({ type: 'REPOSITORY_CLEAR' })
export const fetchRepositoryCount = () => ({ type: 'REPOSITORY_COUNT_FETCH' })
export const fetchRepositoryCountSucceeded = (count: any) => ({ type: 'REPOSITORY_COUNT_FETCH_SUCCEEDED', count })
export const fetchRepositoryCountFailed = (message: any) => ({ type: 'REPOSITORY_COUNT_FETCH_FAILED', message })
export const fetchRepositoryList = ({ user, organization, name, cursor, limit } = {user: '', organization: '', name: '', cursor: '', limit: ''}) => ({ type: 'REPOSITORY_LIST_FETCH', user, organization, name, cursor, limit })
export const fetchRepositoryListSucceeded = (repositories: any) => ({ type: 'REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchRepositoryListFailed = (message: any) => ({ type: 'REPOSITORY_LIST_FETCH_FAILED', message })
export const fetchOwnedRepositoryList = (
{ user, name } = { user: '', name: '' }
) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH', user, name })
export const fetchOwnedRepositoryListSucceeded = (repositories: any) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchOwnedRepositoryListFailed = (message: any) => ({ type: 'OWNED_REPOSITORY_LIST_FETCH_FAILED', message })
export const fetchJoinedRepositoryList = (
{ user, name } = { user: '', name: '' }
) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH', user, name })
export const fetchJoinedRepositoryListSucceeded = (repositories: any) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH_SUCCEEDED', repositories })
export const fetchJoinedRepositoryListFailed = (message: any) => ({ type: 'JOINED_REPOSITORY_LIST_FETCH_FAILED', message })
export const fetchForeignInterfaces = ({ id, itf } = {id: '', itf: ''}) => ({ type: 'FOREIGN_INTERFACE_FETCH', id, itf })
export const fetchForeignInterfacesSuccessed = (data: any) => ({ type: 'FOREIGN_INTERFACE_FETCH_SUCCEEDED', data })
export const fetchForeignInterfacesFailed = (message: any) => ({ type: 'FOREIGN_INTERFACE_FETCH_FAILED', message })
export const addForeignRoomCase = ({ id, itf, name } = { id: '', itf: '', name: ''}) => ({ type: 'ADD_FOREIGN_ROOM_CASE', id, itf, name })
export const addForeignRoomCaseSuccessed = ({ id, itf } = { id: '', itf: ''}) => ({ type: 'ADD_FOREIGN_ROOM_CASE_SUCCEEDED', id, itf })
export const addForeignRoomCaseFailed = ({ id, itf, message }: any) => ({ type: 'ADD_FOREIGN_ROOM_CASE_FAILED', id, itf, message })

@ -0,0 +1,66 @@
import { RouterState } from 'connected-react-router'
export interface RootState {
auth: any
router: RouterState
repository: any
repositories: any
organization: any
organizations: any
ownedRepositories: any
ownedOrganizations: any
joinedOrganizations: any
joinedRepositories: any
users: any
counter: any
logs: any
foreign: any
loading: boolean
}
export interface Organization {
id: number
name: string
description?: string
logo?: string
/** true: 公开, false: 私有 */
visibility?: boolean
creatorId?: number
ownerId?: number
memberIds?: number[]
members?: User[]
owner?: User
newOwner?: User
}
export interface User {
id: number
fullname: string
email: string
}
export interface INumItem {
value: number
label: string
}
export interface IConfig {
serve: string
keys: string[]
session: {
key: string
}
}

@ -17,7 +17,6 @@ body
line-height: 1.5;
font-family: $font-family;
-webkit-font-smoothing: antialiased;
color: #24292e;
html, body
height: 100%;
@ -45,7 +44,6 @@ html, body
button, input, optgroup, select, textarea
font-size: 1.2rem;
font-family: $font-family;
color: #24292e;
-webkit-font-smoothing: antialiased;
// *
@ -86,4 +84,7 @@ pre
border: 1px solid #DDDDDD;
border-radius: 16px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-right-radius: 0;
.mr8
margin-right: 8px;

@ -4,7 +4,6 @@
$brand: #4A7BF7;
$brand-hover: #4471E3;
$nav-bg: #24292e;
$nav-color: rgba(255, 255, 255, 0.75);
$nav-hover: rgba(255, 255, 255, 1);
$table-hover: #F0F3FA;

@ -2,16 +2,15 @@ import React from 'react'
import { connect } from 'react-redux'
import { renderRoutes } from 'react-router-config'
const Account = ({ route, match, location }) => (
const Account = ({ route }: any) => (
<article>
{route && renderRoutes(route.routes)}
</article>
)
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
null,
mapDispatchToProps
)(Account)

@ -1,91 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { login } from '../../actions/account'
import { Link } from 'react-router-dom'
import { serve } from '../../config'
import Mock from 'mockjs'
import './LoginForm.css'
//
const mockUser = process.env.NODE_ENV === 'development'
? () => Mock.mock({
email: 'admin@rap2.com',
password: 'admin'
})
: () => ({
email: '',
password: ''
})
mockUser.captchaId = Date.now()
//
class LoginForm extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = mockUser()
}
render () {
return (
<section className='LoginForm'>
<div className='header'>
<span className='title'>登录</span>
</div>
<form onSubmit={this.handleSubmit}>
<div className='body'>
<div className='form-group'>
<label>邮箱</label>
<input value={this.state.email} onChange={e => this.setState({ email: e.target.value })} className='form-control' placeholder='Email' autoFocus='true' required />
</div>
<div className='form-group'>
<label>密码</label>
<input value={this.state.password} type='password' onChange={e => this.setState({ password: e.target.value })} className='form-control' placeholder='Password' required />
</div>
<div className='form-group'>
<label>验证码</label>
<input onChange={e => this.setState({ captcha: e.target.value })} className='form-control' placeholder='验证码' required />
<img src={`${serve}/captcha?t=${this.state.captchaId || ''}`} onClick={e => this.setState({ captchaId: Date.now() })} alt='captcha' />
</div>
</div>
<div className='footer'>
<button type='submit' className='btn btn-primary w80 mr10'>提交</button>
<Link to='/account/register' className='mr10'>注册</Link>
<Link to='/account/reset'>密码找回</Link>
</div>
{this.props.auth.message &&
<div className='alert alert-danger fade show' role='alert'>
{this.props.auth.message}
</div>
}
</form>
</section>
)
}
handleSubmit = (e) => {
let { history, onLogin } = this.props
e.preventDefault()
onLogin(this.state, () => {
let { pathname } = history.location
if (pathname !== '/account/login') history.push(pathname) //
else history.push('/') //
})
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({
onLogin: login
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginForm)

@ -1,22 +1,27 @@
@import "../../assets/variables.sass";
// +
.LoginForm
position: fixed;
left: 50%;
top: 50%;
width: 25rem;
margin-left: -12.5rem;
margin-top: -135px;
border: 1px solid #E6E6E6;
border-radius: .5rem;
.header
padding: 1.5rem 3rem;
border-bottom: 1px solid $border;
.title
font-size: 2rem;
.body
padding: 1.5rem 3rem;
.footer
padding: 1.5rem 3rem;
border-top: 1px solid $border;
.wrapper
background-size: cover;
width: 100%
height: 100%
min-height: 800px
.LoginForm
position: fixed;
left: 50%;
top: 30%;
width: 25rem;
margin-left: -12.5rem;
margin-top: -135px;
border: 1px solid #E6E6E6;
border-radius: .5rem;
.header
padding: 1.5rem 3rem;
border-bottom: 1px solid $border;
.title
font-size: 2rem;
.body
padding: 1.5rem 3rem;
.footer
padding: 1.5rem 3rem;
border-top: 1px solid $border;

@ -0,0 +1,109 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { login } from '../../actions/account'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import './LoginForm.css'
import { RootState } from 'actions/types'
import config from '../../config'
import { Button, Card } from '@material-ui/core'
import { getBGImageUrl } from 'utils/ImageUtils'
import Logo from 'components/layout/Logo'
const { serve } = config
// 模拟数据
const mockUser: any = process.env.NODE_ENV === 'development'
? () => Mock.mock({
email: 'admin@rap2.com',
password: 'admin',
})
: () => ({
email: '',
password: '',
})
mockUser.captchaId = Date.now()
// 展示组件
class LoginForm extends Component<any, any> {
constructor(props: any) {
super(props)
this.state = {
...mockUser(),
bg: getBGImageUrl(),
}
}
render() {
return (
<div className="wrapper" style={{ background: this.state.bg }}>
<Card className="LoginForm">
<div className="header">
<Logo color="#3f51b5" />
</div>
<form onSubmit={this.handleSubmit}>
<div className="body">
<div className="form-group">
<label></label>
<input
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
className="form-control"
placeholder="Email"
autoFocus={true}
required={true}
/>
</div>
<div className="form-group">
<label></label>
<input
value={this.state.password}
type="password"
onChange={e => this.setState({ password: e.target.value })}
className="form-control"
placeholder="Password"
required={true}
/>
</div>
<div className="form-group">
<label></label>
<input onChange={e => this.setState({ captcha: e.target.value })} className="form-control" placeholder="验证码" required={true} />
<img src={`${serve}/captcha?t=${this.state.captchaId || ''}`} onClick={() => this.setState({ captchaId: Date.now() })} alt="captcha" />
</div>
</div>
<div className="footer">
<Button type="submit" className="mr10" variant="contained" color="primary" style={{ marginRight: 10 }}></Button>
<Link to="/account/register" className="mr10"></Link>
{/* <Link to="/account/reset">密码找回</Link> */}
</div>
{this.props.auth.message &&
<div className="alert alert-danger fade show" role="alert">
{this.props.auth.message}
</div>
}
</form>
</Card>
</div>
)
}
handleSubmit = (e: any) => {
const { onLogin } = this.props
e.preventDefault()
const { email, password, captcha } = this.state
onLogin({ email, password, captcha }, () => {
window.location.href = '/'
})
}
}
// 容器组件
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
})
const mapDispatchToProps = ({
onLogin: login,
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginForm)

@ -1,19 +0,0 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
const Nav = ({ route, match, location }) => (
<ul className='rap-navigation'>
<li><NavLink to='/account/users' activeClassName='selected'>User List</NavLink></li>
<li><NavLink to='/account/signin' activeClassName='selected'>Sign In</NavLink></li>
<li><NavLink to='/account/signup' activeClassName='selected'>Sign Up</NavLink></li>
</ul>
)
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Nav)

@ -0,0 +1,18 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
const Nav = () => (
<ul className="rap-navigation">
<li><NavLink to="/account/users" activeClassName="selected">User List</NavLink></li>
<li><NavLink to="/account/signin" activeClassName="selected">Sign In</NavLink></li>
<li><NavLink to="/account/signup" activeClassName="selected">Sign Up</NavLink></li>
</ul>
)
const mapDispatchToProps = ({})
export default connect(
null,
mapDispatchToProps
)(Nav)

@ -1,79 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import { addUser } from '../../actions/account'
import './RegisterForm.css'
//
const mockUser = process.env.NODE_ENV === 'development'
? () => Mock.mock({
fullname: '@CNAME',
email: '@email',
password: '@string(6)',
errMsg: '',
})
: () => ({
fullname: '',
email: '',
password: '',
errMsg: '',
})
//
class RegisterForm extends Component {
constructor (props) {
super(props)
this.state = mockUser()
}
render () {
const { errMsg } = this.state
return (
<section className='RegisterForm'>
<div className='header'>
<span className='title'>注册</span>
</div>
<form className='body' onSubmit={this.handleSubmit}>
<div className='form-group'>
<label>姓名</label>
<input value={this.state.fullname} onChange={e => this.setState({ fullname: e.target.value })} className='form-control' placeholder='Name' autoFocus='true' required />
</div>
<div className='form-group'>
<label>邮箱</label>
<input value={this.state.email} onChange={e => this.setState({ email: e.target.value })} className='form-control' placeholder='Email' required />
</div>
<div className='form-group'>
<label>密码</label>
<input value={this.state.password} onChange={e => this.setState({ password: e.target.value })} type='password' className='form-control' placeholder='Password' required />
</div>
{errMsg && <div className='alert alert-danger'>{errMsg}</div>}
<button type='submit' className='btn btn-primary w140 mr20'>提交</button>
<Link to='/account' className=''>取消</Link>
</form>
</section>
)
}
handleSubmit = (e) => {
let { history, onAddUser } = this.props
e.preventDefault()
onAddUser(this.state, (errorOccurred, errMsg) => {
if (errorOccurred) {
this.setState({
errMsg,
})
} else {
history.push('/') // <Redirect to="/somewhere/else"/>
}
})
}
}
//
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({
onAddUser: addUser
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RegisterForm)

@ -1,9 +1,27 @@
.RegisterForm
width: 30rem;
margin: 0 auto;
.header
margin-bottom: 3rem;
.title
font-size: 2rem;
.body
.footer
@import "../../assets/variables.sass";
// +
.wrapper
background-size: cover;
width: 100%
height: 100%
min-height: 800p
.RegisterForm
position: fixed;
left: 50%;
top: 30%;
width: 25rem;
margin-left: -12.5rem;
margin-top: -135px;
border: 1px solid #E6E6E6;
border-radius: .5rem;
.header
padding: 1.5rem 3rem;
border-bottom: 1px solid $border;
.title
font-size: 2rem;
.body
padding: 1.5rem 3rem;
.footer
padding: 1.5rem 3rem;
border-top: 1px solid $border;

@ -0,0 +1,102 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import { addUser } from '../../actions/account'
import './RegisterForm.css'
import { Button, Card } from '@material-ui/core'
import { getBGImageUrl } from 'utils/ImageUtils'
type State = {
fullname: string
email: string
password: string
bg: string
}
type Props = {
history: any
onAddUser: any
}
// 模拟数据
const mockUser = () =>
Mock.mock({
fullname: '@CNAME',
email: '@email',
password: '@string(6)',
})
// 展示组件
class RegisterForm extends Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
...mockUser(),
bg: getBGImageUrl(),
}
}
render() {
return (
<div className="wrapper" style={{ background: this.state.bg }}>
<Card className="RegisterForm">
<div className="header">
<span className="title"></span>
</div>
<form className="body" onSubmit={this.handleSubmit}>
<div className="form-group">
<label></label>
<input
value={this.state.fullname}
onChange={e => this.setState({ fullname: e.target.value })}
className="form-control"
placeholder="Name"
autoFocus={true}
required={true}
/>
</div>
<div className="form-group">
<label></label>
<input
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
className="form-control"
placeholder="Email"
required={true}
/>
</div>
<div className="form-group">
<label></label>
<input
value={this.state.password}
onChange={e => this.setState({ password: e.target.value })}
type="password"
className="form-control"
placeholder="Password"
required={true}
/>
</div>
<Button type="submit" variant="contained" color="primary" style={{ marginRight: 8 }}> </Button>
<Link to="/account"></Link>
</form>
</Card>
</div>
)
}
handleSubmit = (e: any) => {
const { history, onAddUser } = this.props
e.preventDefault()
onAddUser(this.state, () => {
history.push('/')
})
};
}
// 容器组件
const mapDispatchToProps = {
onAddUser: addUser,
}
export default connect(
null,
mapDispatchToProps
)(RegisterForm)

@ -1,107 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { reset } from '../../actions/account'
import { Link } from 'react-router-dom'
import './ResetForm.css'
//
class LoginForm extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired
}
constructor(props) {
super(props)
this.state = {
email: '',
password: '',
showPassword: false,
newPassword: '',
}
}
render() {
const { showPassword, email, password, newPassword } = this.state
if (newPassword) {
return (
<section className='ResetForm'>
<div className='header'>
<span className='title'>找回密码</span>
</div>
<div className='body'>
<div className='form-group'>
您的密码已重设为: {newPassword}, 请重新登陆
</div>
</div>
</section>
)
}
return (
<section className='ResetForm'>
<div className='header'>
<span className='title'>找回密码</span>
</div>
<form onSubmit={this.handleSubmit}>
<div className='body'>
<div className='form-group'>
<label>邮箱</label>
<input value={email} onChange={e => this.setState({ email: e.target.value })} className='form-control' placeholder='Email' autoFocus='true' required disabled={showPassword} />
</div>
{showPassword && <div>
<div className='form-group'>
<label>验证码</label>
<input value={password} onChange={e => this.setState({ password: e.target.value })} className='form-control' placeholder='验证码' />
</div>
<div className='form-group'>
已发送验证码至您输入的邮箱请输入验证码以重置您的密码
</div>
</div>}
</div>
<div className='footer'>
<button type='submit' className='btn btn-primary w80 mr10'>提交</button>
<Link to='/account/login' className='mr10'>返回</Link>
</div>
{this.props.auth.message &&
<div className='alert alert-danger fade show' role='alert'>
{this.props.auth.message}
</div>
}
</form>
</section>
)
}
handleSubmit = (e) => {
const { onReset } = this.props
const { email, password } = this.state
e.preventDefault()
if (email) {
this.setState({
showPassword: true,
})
onReset(email, password, (result) => {
if (!result.isOk) {
alert(result.errMsg)
return
}
if (result.data) {
this.setState({
newPassword: result.data,
})
}
})
}
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({
onReset: reset,
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginForm)

@ -1,76 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { updateUser } from '../../actions/account'
import './UpdateForm.css'
//
class UpdateForm extends Component {
static propTypes = {
auth: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
fullname: '',
email: '',
password: ''
}
}
render () {
const auth = this.props.auth
return (
<section className='UpdateForm'>
<div className='header'>
<span className='title'>个人资料修改</span>
</div>
<form className='body' onSubmit={this.handleSubmit}>
<div className='form-group'>
<label>姓名</label>
<input value={auth.fullname} className='form-control' placeholder='Name' disabled />
</div>
<div className='form-group'>
<label>邮箱</label>
<input value={auth.email} className='form-control' placeholder='Email' disabled />
</div>
<div className='form-group'>
<label>密码</label>
<input value={this.state.password} onChange={e => this.setState({ password: e.target.value })} type='password' className='form-control' placeholder='Password' required autoFocus='true' />
</div>
<div className='form-group'>
<label>确认密码</label>
<input value={this.state.passwordConfirm} onChange={e => this.setState({ passwordConfirm: e.target.value })} type='password' className='form-control' placeholder='Password' required autoFocus='true' />
</div>
<button type='submit' className='btn btn-primary w140 mr20'>提交</button>
<a onClick={e => { e.preventDefault(); window.history.go(-1) }} >取消</a>
</form>
</section>
)
}
handleSubmit = (e) => {
let { history, onUpdateUser } = this.props
e.preventDefault()
if (this.state.password && this.state.password === this.state.passwordConfirm) {
onUpdateUser({ password: this.state.password }, (errMsg) => {
if (errMsg) {
window.alert(errMsg)
} else {
history.push('/')
}
})
} else {
window.alert('两次密码不一致,请检查后重新输入')
}
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({
onUpdateUser: updateUser
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(UpdateForm)

@ -1,13 +1,13 @@
import React from 'react'
import { Link } from 'react-router-dom'
const User = ({ match, id, fullname, email, onDeleteUser }) => (
const User = ({ match, id, fullname, email, onDeleteUser }: any) => (
<tr>
<td>{id}</td>
<td>{fullname}</td>
<td>{email}</td>
<td>
<Link to={match.url} onClick={e => onDeleteUser(id)}>X</Link>
<Link to={match.url} onClick={() => onDeleteUser(id)}>X</Link>
</td>
</tr>
)

@ -4,14 +4,15 @@ import { connect } from 'react-redux'
import Pagination from '../utils/Pagination'
import { addUser, deleteUser, fetchUserList } from '../../actions/account'
import './UserList.css'
import { RootState } from 'actions/types'
// 展示组件
const UserList = ({ history, match, location, users, onAddUser, onDeleteUser, onFetchUserList, tmpl = {} }) => (
<section className='UserList'>
<div className='header'>
<span className='title'></span>
const UserList = ({ match, location, users, onDeleteUser }: any) => (
<section className="UserList">
<div className="header">
<span className="title"></span>
</div>
<nav className='toolbar clearfix'>
<nav className="toolbar clearfix">
{/* <div className='float-left'>
<Link to='/account/register' className='btn btn-success w140'><span className=''>&#xe654;</span></Link>
</div>
@ -19,17 +20,17 @@ const UserList = ({ history, match, location, users, onAddUser, onDeleteUser, on
<button onClick={e => onFetchUserList(location.params)} className='btn btn-default'>R</button>
</div> */}
</nav>
<div className='body'>
<table className='table'>
<div className="body">
<table className="table">
<thead>
<tr>
<th></th>
<th></th>
<th className='w100'></th>
<th className="w100"></th>
</tr>
</thead>
<tbody>
{users.data.map(user =>
{users.data.map((user: any) =>
<tr key={user.id}>
<td>
<Link to={`/repository?user=${user.id}`}>#{user.id} {user.fullname}</Link>
@ -37,7 +38,7 @@ const UserList = ({ history, match, location, users, onAddUser, onDeleteUser, on
<td>{user.email}</td>
<td>
<span style={{ cursor: 'not-allowed' }}>
<Link to={match.url} onClick={e => onDeleteUser(user.id)} className='operation disabled'></Link>
<Link to={match.url} onClick={() => onDeleteUser(user.id)} className="operation disabled"></Link>
</span>
</td>
</tr>
@ -45,20 +46,20 @@ const UserList = ({ history, match, location, users, onAddUser, onDeleteUser, on
</tbody>
</table>
</div>
<div className='footer'>
<div className="footer">
<Pagination location={location} calculated={users.pagination} />
</div>
</section>
)
// 容器组件
const mapStateToProps = (state) => ({
users: state.users
const mapStateToProps = (state: RootState) => ({
users: state.users,
})
const mapDispatchToProps = ({
onAddUser: addUser,
onDeleteUser: deleteUser,
onFetchUserList: fetchUserList
onFetchUserList: fetchUserList,
})
export default connect(
mapStateToProps,

@ -1,5 +1,6 @@
.APIList
padding: 2rem;
margin: 16px;
> .header
margin-bottom: 2rem;
.title

@ -1,6 +1,7 @@
import React from 'react'
import { serve } from '../../relatives/services/constant'
import './API.css'
import { Paper } from '@material-ui/core'
const ExampleJQuery = () => (
<div>
@ -10,7 +11,7 @@ const ExampleJQuery = () => (
<li>RAP jQuery</li>
</ul>
<h4></h4>
<pre className='code-example'>
<pre className="code-example">
{
'<script src="jquery.min.js"></script>\n' +
'<script src="http://rap2api.taobao.org/app/plugin/:projectId"></script>\n' +
@ -31,70 +32,84 @@ const ExampleJQuery = () => (
// DONE 2.3 区分请求和响应作用域
class API extends React.Component {
constructor (props) {
type State = {
showExampleJQuery: boolean;
}
type Props = any
class API extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
showExampleJQuery: false
showExampleJQuery: false,
}
}
render () {
render() {
return (
<section className='APIList'>
<div className='header'>
<span className='title'></span>
<Paper className="APIList">
<div className="header">
<span className="title"></span>
</div>
<div className='body'>
<div className='API'>
<div className="body">
<div className="API">
<ul>
<li>
<a href='https://github.com/thx/rap2-delos/wiki'>https://github.com/thx/rap2-delos/wiki</a>
<a href="https://github.com/thx/rap2-delos/wiki">https://github.com/thx/rap2-delos/wiki</a>
</li>
</ul>
</div>
</div>
<div className='header'>
<span className='title'>API</span>
<div className="header">
<span className="title">API</span>
</div>
<div className='body'>
<div className='API'>
<div className='title'>JSON</div>
<div className="body">
<div className="API">
<div className="title">JSON</div>
<ul>
<li><code>{serve}/repository/get?id=:repositoryId</code></li>
</ul>
</div>
<div className='API'>
<div className='title'>JSON</div>
<div className="API">
<div className="title">JSON</div>
<ul>
<li><code>{serve}/interface/get?id=:interfaceId</code></li>
</ul>
</div>
<div className='API'>
<div className='title'>JS</div>
<div className="API">
<div className="title">JS</div>
<ul>
<li><span className='label'></span><code>{serve}/app/plugin/:repositories</code></li>
<li><span className='label'>jQuery </span><code>{serve}/libs/jquery.rap.js</code><button className='btn btn-secondary btn-sm ml8' onClick={
e => {
e.preventDefault()
this.setState((prevState, props) => {
return { showExampleJQuery: !prevState.showExampleJQuery }
})
}
}></button></li>
<li><span className="label"></span><code>{serve}/app/plugin/:repositories</code></li>
<li><span className="label">jQuery </span><code>{serve}/libs/jquery.rap.js</code>
<button
className="btn btn-secondary btn-sm ml8"
onClick={
e => {
e.preventDefault()
this.setState((prevState) => {
return { showExampleJQuery: !prevState.showExampleJQuery }
})
}
}
>
</button>
</li>
{this.state.showExampleJQuery && <ExampleJQuery />}
<li><span className='label'>Mock.js </span><code>{serve}/libs/mock.rap.js</code></li>
<li><span className='label'>fetch </span><code>{serve}/libs/fetch.rap.js</code></li>
<li><span className="label">Mock.js </span><code>{serve}/libs/mock.rap.js</code></li>
<li><span className="label">fetch </span><code>{serve}/libs/fetch.rap.js</code></li>
</ul>
</div>
<div className='API'>
<div className='title'>JSON</div>
<div className="API">
<div className="title">JSON</div>
<ul>
<li>
<code>{serve}/app/mock/data/:interfaceId?scope=response|request</code>
<table className='table table-bordered mt12'>
<table className="table table-bordered mt12">
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th style={{ width: '140px' }}><code>scope</code></th>
<th></th>
</tr>
</thead>
@ -113,15 +128,15 @@ class API extends React.Component {
<li><code>{serve}/app/mock/:repositoryId/:method/:url</code></li>
</ul>
</div>
<div className='API'>
<div className='title'>JSON</div>
<div className="API">
<div className="title">JSON</div>
<ul>
<li>
<code>{serve}/app/mock/template/:interfaceId?scope=response|request</code>
<table className='table table-bordered mt12'>
<table className="table table-bordered mt12">
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th style={{ width: '120px' }}><code>scope</code></th>
<th></th>
</tr>
</thead>
@ -139,15 +154,15 @@ class API extends React.Component {
</li>
</ul>
</div>
<div className='API'>
<div className='title'>JSON Schema</div>
<div className="API">
<div className="title">JSON Schema</div>
<ul>
<li>
<code>{serve}/app/mock/schema/:interfaceId?scope=response|request</code>
<table className='table table-bordered mt12'>
<table className="table table-bordered mt12">
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th style={{ width: '120px' }}><code>scope</code></th>
<th></th>
</tr>
</thead>
@ -166,7 +181,7 @@ class API extends React.Component {
</ul>
</div>
</div>
</section>
</Paper>
)
}
}

@ -1,107 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link } from '../../family'
import { Spin } from '../utils'
import { serve } from '../../relatives/services/constant'
import Mock from 'mockjs'
import './Checker.css'
class Checker extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
repository: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
mod: null,
itf: null,
target: serve
}
}
render () {
let { repository } = this.props
if (repository.fetching) return <Spin />
repository = repository.data
let mod = this.state.mod || repository.modules[0]
let itf = this.state.itf || mod.interfaces[0]
return (
<section className='Checker'>
<div className='card-mods clearfix'>
<span className='card-title'>模块</span>
{repository.modules.map(item =>
<Link key={item.id} to='' onClick={e => this.switchMod(e, item)} className={item.id === mod.id ? 'active' : ''}>{item.name}</Link>
)}
</div>
<div className='card-itfs clearfix'>
<span className='card-title'>接口</span>
{mod.interfaces.map(item =>
<Link key={item.id} to='' onClick={e => this.switchItf(e, item)} className={item.id === itf.id ? 'active' : ''}>{item.name}</Link>
)}
</div>
<div>
<input value={this.state.target} onChange={e => this.setState({ target: e.target.value })} className='form-control' />
</div>
<div className='card-result'>
<div className='card-title'>{`${serve}/app/mock/data/${itf.id}`}</div>
<pre>{JSON.stringify(this.state.result, null, 2)}</pre>
</div>
</section>
)
}
componentWillReceiveProps (nextProps) {
let { repository } = nextProps
repository = repository.data
if (!repository.id) return
let mod = this.state.mod || repository.modules[0]
let itf = this.state.itf || mod.interfaces[0]
fetch(`${serve}/app/mock/data/${itf.id}`)
.then(res => res.json())
.then(json => {
this.setState({ result: json })
})
}
switchMod = (e, mod) => {
e.preventDefault()
this.setState({ mod })
}
switchItf = (e, itf) => {
e.preventDefault()
this.setState({ itf }, () => {
this.handleRequest()
})
}
handleRequest = () => {
let { repositoryId, method, url } = this.state.itf
let target = `${this.state.target}/app/mock/${repositoryId}/${method}/${url}`
let proxy = `${serve}/proxy?target=${target}`
let requests = [
fetch(`${serve}/app/mock/schema/${this.state.itf.id}`).then(res => res.json()),
fetch(proxy).then(res => res.json())
]
Promise.all(requests).then(([schema, data]) => {
let { Diff, Assert } = Mock.valid
let nextMatch = Assert.match
Assert.match = function (type, path, actual, expected, result, message) {
if (typeof expected === 'string') expected = eval('(' + expected + ')') // eslint-disable-line no-eval
nextMatch(type, path, actual, expected, result, message)
}
var result = Diff.diff(schema, data)
for (var i = 0; i < result.length; i++) {
console.warn(Assert.message(result[i]))
}
})
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth,
repository: state.repository
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Checker)

@ -0,0 +1,110 @@
import React, { Component } from 'react'
// eslint-disable-next-line
import { connect, Link } from '../../family'
import { Spin } from '../utils'
import { serve } from '../../relatives/services/constant'
import Mock from 'mockjs'
import './Checker.css'
import { RootState } from 'actions/types'
type CheckerProps = {
store: object;
repository: any;
}
type CheckerState = {
result?: any;
itf?: any;
mod: any;
target: any
}
class Checker extends Component<CheckerProps, CheckerState> {
constructor(props: any) {
super(props)
this.state = {
mod: null,
itf: null,
target: serve,
}
}
render() {
let { repository } = this.props
if (repository.fetching) { return <Spin /> }
repository = repository.data
const mod = this.state.mod || repository.modules[0]
const itf = this.state.itf || mod.interfaces[0]
return (
<section className="Checker">
<div className="card-mods clearfix">
<span className="card-title"></span>
{repository.modules.map((item: any) => (
<Link key={item.id} to="" onClick={e => this.switchMod(e, item)} className={item.id === mod.id ? 'active' : ''}>
{item.name}
</Link>
))}
</div>
<div className="card-itfs clearfix">
<span className="card-title"></span>
{mod.interfaces.map((item: any) => (
<Link key={item.id} to="" onClick={e => this.switchItf(e, item)} className={item.id === itf.id ? 'active' : ''}>
{item.name}
</Link>
))}
</div>
<div>
<input value={this.state.target} onChange={e => this.setState({ target: e.target.value })} className="form-control" />
</div>
<div className="card-result">
<div className="card-title">{`${serve}/app/mock/data/${itf.id}`}</div>
<pre>{JSON.stringify(this.state.result, null, 2)}</pre>
</div>
</section>
)
}
componentWillReceiveProps(nextProps: any) {
let { repository } = nextProps
repository = repository.data
if (!repository.id) { return }
const mod = this.state.mod || repository.modules[0]
const itf = this.state.itf || mod.interfaces[0]
fetch(`${serve}/app/mock/data/${itf.id}`)
.then(res => res.json())
.then(json => {
this.setState({ result: json })
})
}
switchMod = (e: any, mod: any) => {
e.preventDefault()
this.setState({ mod })
};
switchItf = (e: any, itf: any) => {
e.preventDefault()
this.setState({ itf }, () => {
this.handleRequest()
})
};
handleRequest = () => {
const { repositoryId, method, url } = this.state.itf
const target = `${this.state.target}/app/mock/${repositoryId}/${method}/${url}`
const proxy = `${serve}/proxy?target=${target}`
const requests = [fetch(`${serve}/app/mock/schema/${this.state.itf.id}`).then(res => res.json()), fetch(proxy).then(res => res.json())]
Promise.all(requests).then(([schema, data]) => {
const { Diff, Assert } = Mock.valid as any
const nextMatch = Assert.match
Assert.match = (type: any, path: any, actual: any, expected: any, result: any, message: any) => {
// eslint-disable-next-line
if (typeof expected === 'string') { expected = eval('(' + expected + ')') }
nextMatch(type, path, actual, expected, result, message)
}
const result = Diff.diff(schema, data)
for (const i of result) {
console.warn(Assert.message(i))
}
})
};
}
// 容器组件
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
repository: state.repository,
})
const mapDispatchToProps = {}
export default connect(mapStateToProps, mapDispatchToProps)(Checker)

@ -1,29 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import './Footer.css'
const Footer = ({ counter = {} }) => (
<div className='Footer'>
v{counter.version}
{/* <span className='ml10 mr10 color-c'>|</span>
{counter.users} 人正在使用 RAP
<span className='ml10 mr10 color-c'>|</span>
今日 Mock 服务被调用 {counter.mock} */}
<ul className='friend_links'>
<li><a href='http://rap.alibaba-inc.com/' target='_blank' rel='noopener noreferrer'>RAP0.x</a></li>
<li><a href='http://mockjs.com/' target='_blank' rel='noopener noreferrer'>Mock.js</a></li>
<li><a href='https://thx.github.io/' target='_blank' rel='noopener noreferrer'>THX</a></li>
<li><a href='https://fe.alimama.net/thx/30/' target='_blank' rel='noopener noreferrer'>MMFE</a></li>
</ul>
</div>
)
const mapStateToProps = (state) => ({
counter: state.counter
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Footer)

@ -0,0 +1,30 @@
import React from 'react'
import { connect } from 'react-redux'
import './Footer.css'
import { RootState } from 'actions/types'
const Footer = ({ counter = {} }: { counter: any }) => (
<div className="Footer">
{counter && counter.version}
{/* <span className='ml10 mr10 color-c'>|</span>
{counter.users} 使 RAP
<span className='ml10 mr10 color-c'>|</span>
Mock {counter.mock} */}
<ul className="friend_links">
<li><a href="http://rap.alibaba-inc.com/" target="_blank" rel="noopener noreferrer">RAP0.x</a></li>
<li><a href="http://mockjs.com/" target="_blank" rel="noopener noreferrer">Mock.js</a></li>
<li><a href="https://thx.github.io/" target="_blank" rel="noopener noreferrer">THX</a></li>
<li><a href="https://fe.alimama.net/thx/30/" target="_blank" rel="noopener noreferrer">MMFE</a></li>
</ul>
</div>
)
const mapStateToProps = (state: RootState) => ({
counter: state.counter,
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Footer)

@ -0,0 +1,30 @@
import grey from '@material-ui/core/colors/grey'
import { StyleRules, Theme } from '@material-ui/core/styles'
const GlobalStyles = (theme: Theme) => ({
'@global': {
'body': {
margin: 0,
padding: 0,
backgroundColor: grey[200],
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif;',
},
'.ml1': {
marginLeft: theme.spacing(1),
},
'.mr1': {
marginRight: theme.spacing(1),
},
'ol': {
padding: `0 ${theme.spacing(2)}px`,
},
'ul': {
padding: `0 ${theme.spacing(2)}px`,
},
'li': {
padding: `${theme.spacing(1)}px 0`,
},
},
}) as StyleRules
export default GlobalStyles

@ -1,61 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { login, logout } from '../../actions/account'
import Navigation from './Navigation'
import './Header.css'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import './nprogress.css'
const LoginAction = () => (
<Link to='/account/login'>Sign in</Link>
)
const RegisterAction = () => (
<Link to='/account/register'>Sign up</Link>
)
/* eslint-enable no-unused-vars */
const Header = ({ match, location, onLogout, fetching, user = {} }) => {
document.body.style.cursor = fetching ? 'wait' : 'default' // TODO 2.3 APP
fetching ? NProgress.start() : NProgress.done()
return (
<section className='Header'>
<nav className='clearfix'>
<Navigation />
{user.id ? (
<ul className='nav-actions list-inline float-right'>
<li><Link to='/account/update' className='name'>{user.fullname}</Link></li>
<li><a href='/account/login' onClick={onLogout}>退出</a></li>
</ul>
) : (
<ul className='nav-actions list-inline float-right'>
<li><LoginAction /></li>
<li><RegisterAction /></li>
</ul>
)}
</nav>
</section>
)
}
//
const mapStateToProps = (state) => ({
fetching: (() => {
let fetching = 0
for (let key in state) {
if (state[key].fetching) fetching += 1
}
return fetching
})(), // state.fetching
user: state.auth
})
const mapDispatchToProps = ({
onLogin: login,
onLogout: logout
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Header)

@ -1,38 +0,0 @@
@import "../../assets/variables.sass";
.Header
background-color: $nav-bg;
color: white;
font-size: 1.4rem;
line-height: 4rem;
vertical-align: middle;
// margin-bottom: 2rem;
padding: 0 2rem;
ul.nav-links
float: left;
ul.nav-actions
float: right;
ul.nav-links,
ul.nav-actions
margin: 0;
padding: 0;
list-style: none;
> li
float: left;
padding: 0 1rem;
> a
color: white;
opacity: 0.5;
&:hover, &.selected
border: none;
font-weight: bold;
opacity: 1;
> a.logo
opacity: 1;
font-weight: bold;
.avatar
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
.name
margin-left: .5rem;

@ -0,0 +1,39 @@
import React from 'react'
import { connect } from 'react-redux'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import './nprogress.css'
import MainMenu from 'components/layout/MainMenu'
const Header = ({ fetching, user = {} }: any) => {
if (!user || !user.id) {
return null
}
document.body.style.cursor = fetching ? 'wait' : 'default' // TODO 2.3 应该有更好的方式监听整个 APP 是否有未完成的请求!
fetching ? NProgress.start() : NProgress.done()
return (
<section className="Header">
<nav className="clearfix">
<MainMenu user={user} />
</nav>
</section>
)
}
const mapStateToProps = (state: any) => ({
fetching: (() => {
let fetching = 0
for (const key in state) {
if (state[key] && state[key].fetching) { fetching += 1 }
}
return fetching
})(), // state.fetching
user: state.auth,
})
const mapDispatchToProps = ({
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Header)

@ -0,0 +1,42 @@
import React from 'react'
import { Button, makeStyles, createStyles, CircularProgress } from '@material-ui/core'
import { ButtonProps } from '@material-ui/core/Button'
import { green } from '@material-ui/core/colors'
const useStyles = makeStyles(() =>
createStyles({
root: {
position: 'relative',
display: 'inline',
},
buttonProgress: {
color: green[500],
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12,
},
})
)
interface Props extends ButtonProps {
label: string
}
export default function LoadingButton(props: Props) {
const { children, label, ...rest } = props
const classes = useStyles()
const loading = props.disabled
return (
<div className={classes.root}>
<Button
{...rest}
>
{loading ? '处理中...' : label}
{children}
</Button>
{loading && <CircularProgress size={24} className={classes.buttonProgress} />}
</div>
)
}

@ -0,0 +1,21 @@
import createMuiTheme from '@material-ui/core/styles/createMuiTheme'
export const theme = {
palette: {
},
overrides: {
},
}
const MuiTheme = createMuiTheme({
overrides: {
MuiTableCell: {
root: {
padding: '0 16px',
},
},
},
...theme,
})
export default MuiTheme

@ -1,19 +0,0 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
import { GoHome, GoRepo, GoOrganization, GoPulse, GoPlug } from 'react-icons/lib/go'
export default () => (
<ul className='nav-links'>
<li><NavLink exact to='/' activeClassName='selected'><GoHome /> 首页</NavLink></li>
<li><NavLink to='/repository' activeClassName='selected'><GoRepo /> 仓库</NavLink></li>
<li><NavLink to='/organization' activeClassName='selected'><GoOrganization /> 团队</NavLink></li>
<li><NavLink to='/api' activeClassName='selected'><GoPlug /> 接口</NavLink></li>
<li><NavLink to='/status' activeClassName='selected'><GoPulse /> 状态</NavLink></li>
{/* <li><NavLink to='/manage' activeClassName='selected'>管理</NavLink></li> */}
{/* <li><NavLink to='/account' activeClassName='selected'>Users</NavLink></li>
<li><NavLink to='/organization' activeClassName='selected'>Organization</NavLink></li>
<li><NavLink to='/workspace' activeClassName='selected'>Workspace</NavLink></li>
<li><NavLink to='/analytics' activeClassName='selected'>Analytics</NavLink></li>
<li><NavLink to='/utils' activeClassName='selected'>Utils</NavLink></li> */}
</ul>
)

@ -0,0 +1,13 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
import { GoHome, GoRepo, GoOrganization, GoPulse, GoPlug } from 'react-icons/go'
export default () => (
<ul className="nav-links">
<li><NavLink exact={true} to="/" activeClassName="selected"><GoHome /> </NavLink></li>
<li><NavLink to="/repository" activeClassName="selected"><GoRepo /> </NavLink></li>
<li><NavLink to="/organization" activeClassName="selected"><GoOrganization /> </NavLink></li>
<li><NavLink to="/api" activeClassName="selected"><GoPlug /> </NavLink></li>
<li><NavLink to="/status" activeClassName="selected"><GoPulse /> </NavLink></li>
</ul>
)

@ -0,0 +1,275 @@
import React, { HTMLAttributes } from 'react'
import { Chip, Typography, MenuItem, TextField, NoSsr, Paper } from '@material-ui/core'
import { emphasize } from '@material-ui/core/styles/colorManipulator'
import CancelIcon from '@material-ui/icons/Cancel'
import { createStyles, makeStyles, useTheme, Theme } from '@material-ui/core/styles'
import { BaseTextFieldProps } from '@material-ui/core/TextField'
import Select from 'react-select'
import clsx from 'clsx'
import { NoticeProps, MenuProps } from 'react-select/src/components/Menu'
import { ControlProps } from 'react-select/src/components/Control'
import { PlaceholderProps } from 'react-select/src/components/Placeholder'
import { SingleValueProps } from 'react-select/src/components/SingleValue'
import { ValueContainerProps } from 'react-select/src/components/containers'
import { MultiValueProps } from 'react-select/src/components/MultiValue'
import AsyncSelect from 'react-select/async'
import { OptionProps } from 'react-select/src/components/Option'
import { INumItem, User } from '../../actions/types'
const debounce = require('debounce-promise')
interface OptionType {
label: string
value: string
}
export type SelectedItem = Pick<User, 'id' | 'fullname'>
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
marginBottom: theme.spacing(1),
},
input: {
display: 'flex',
padding: 0,
height: 'auto',
},
valueContainer: {
display: 'flex',
flexWrap: 'wrap',
flex: 1,
alignItems: 'center',
overflow: 'hidden',
},
chip: {
margin: theme.spacing(0.5, 0.25),
},
chipFocused: {
backgroundColor: emphasize(
theme.palette.type === 'light' ? theme.palette.grey[300] : theme.palette.grey[700],
0.08
),
},
noOptionsMessage: {
padding: theme.spacing(1, 2),
},
singleValue: {
fontSize: 16,
},
placeholder: {
position: 'absolute',
left: 2,
bottom: 6,
fontSize: 16,
},
paper: {
position: 'absolute',
zIndex: 1,
marginTop: theme.spacing(1),
left: 0,
right: 0,
},
divider: {
height: theme.spacing(2),
},
})
)
function NoOptionsMessage(props: NoticeProps<OptionType>) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.noOptionsMessage}
{...props.innerProps}
>
{props.children}
</Typography>
)
}
type InputComponentProps = Pick<BaseTextFieldProps, 'inputRef'> & HTMLAttributes<HTMLDivElement>
function inputComponent({ inputRef, ...props }: InputComponentProps) {
return <div ref={inputRef} {...props} />
}
function Control(props: ControlProps<OptionType>) {
return (
<TextField
style={{ minWidth: props.selectProps.minWidth || 350 }}
InputProps={{
inputComponent,
inputProps: {
className: props.selectProps.classes.input,
inputRef: props.innerRef,
children: props.children,
...props.innerProps,
},
}}
{...props.selectProps.TextFieldProps}
/>
)
}
function Option(props: OptionProps<OptionType>) {
return (
<MenuItem
ref={props.innerRef}
selected={props.isFocused}
component="div"
style={{
fontWeight: props.isSelected ? 500 : 400,
}}
{...props.innerProps}
>
{props.children}
</MenuItem>
)
}
function Placeholder(props: PlaceholderProps<OptionType>) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.placeholder}
{...props.innerProps}
>
{props.children}
</Typography>
)
}
function SingleValue(props: SingleValueProps<OptionType>) {
return (
<Typography className={props.selectProps.classes.singleValue} {...props.innerProps}>
{props.children}
</Typography>
)
}
function ValueContainer(props: ValueContainerProps<OptionType>) {
return <div className={props.selectProps.classes.valueContainer}>{props.children}</div>
}
function MultiValue(props: MultiValueProps<OptionType>) {
return (
<Chip
tabIndex={-1}
label={props.children}
className={clsx(props.selectProps.classes.chip, {
[props.selectProps.classes.chipFocused]: props.isFocused,
})}
onDelete={props.removeProps.onClick}
deleteIcon={<CancelIcon {...props.removeProps} />}
/>
)
}
function Menu(props: MenuProps<OptionType>) {
return (
<Paper square={true} className={props.selectProps.classes.paper} {...props.innerProps}>
{props.children}
</Paper>
)
}
const components = {
Control,
Menu,
MultiValue,
NoOptionsMessage,
Option,
Placeholder,
SingleValue,
ValueContainer,
}
interface Props {
loadOptions?: (input: string) => Promise<INumItem[]>
options?: INumItem[]
isMulti?: boolean
selected?: INumItem[]
value?: INumItem[]
minWidth?: number
onChange: (selected: SelectedItem[]) => void
}
function UserList(props: Props) {
const classes = useStyles()
const theme = useTheme()
const { loadOptions, isMulti, onChange, options, selected, value, minWidth } = props
const selectStyles = {
input: (base: any) => ({
...base,
color: theme.palette.text.primary,
'& input': {
font: 'inherit',
},
}),
}
const commonProps: any = {
minWidth,
cacheOptions: true,
defaultValue: selected || [],
isMulti: isMulti || false,
noOptionsMessage: ({ inputValue }: { inputValue: string }) => {
return inputValue && inputValue.trim()
? '搜不到数据'
: '请输入检索关键字'
},
onChange: (vals: INumItem[] | INumItem) => {
if (Array.isArray(vals)) {
onChange(vals.map(x => ({ id: x.value, fullname: x.label })))
} else {
onChange([{ id: vals.value, fullname: vals.label }])
}
},
placeholder: `请选择(${isMulti ? '多选' : '单选'})`,
}
if (value) {
(commonProps as any).value = value
}
if (loadOptions) {
return (
<div className={classes.root}>
<NoSsr>
<AsyncSelect
classes={classes}
loadOptions={debounce(loadOptions, 250)}
components={components}
{...commonProps}
/>
</NoSsr>
</div>
)
} else if (options) {
return (
<div className={classes.root}>
<NoSsr>
<Select
classes={classes}
styles={selectStyles}
TextFieldProps={{
InputLabelProps: {
shrink: true,
},
}}
options={options as any}
components={components}
{...commonProps}
/>
</NoSsr>
</div>
)
} else {
throw new Error(
'One of props.options and props.loadOptions is required.'
)
}
}
export default UserList

@ -1,65 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, Link, StoreStateRouterLocationURI, URI } from '../../family'
import { GoAlert } from 'react-icons/lib/go'
class DuplicatedInterfacesWarning extends Component {
static propTypes = {
repository: PropTypes.object.isRequired
}
static contextTypes = {
store: PropTypes.object.isRequired
}
static parseDuplicatedInterfaces (repository) {
let counter = {}
for (let mod of repository.modules) {
for (let itf of mod.interfaces) {
let key = `${itf.method} ${itf.url}`
if (!counter[key]) counter[key] = []
counter[key] = [...counter[key], { ...itf, mod }]
}
}
let duplicated = []
for (let key in counter) {
if (counter[key].length > 1) {
duplicated.push(counter[key])
}
}
return duplicated
}
static printDuplicatedInterfacesWarning (duplicated) {
duplicated.forEach(interfaces => {
let key = `${interfaces[0].method} ${interfaces[0].url}`
console.group('警告:检测到重复接口 ' + key)
interfaces.forEach(itf => {
console.warn(`#${itf.id} ${itf.method} ${itf.url}`)
})
console.groupEnd('警告:检测到重复接口 ' + key)
})
}
render () {
let { store } = this.context
let { repository } = this.props
if (!repository) return null
let duplicated = DuplicatedInterfacesWarning.parseDuplicatedInterfaces(repository)
if (!duplicated.length) return null
let uri = StoreStateRouterLocationURI(store).removeSearch('page').removeSearch('itf')
return (
<div className='DuplicatedInterfacesWarning'>
{duplicated.map((interfaces, index) =>
<div key={index} className='alert alert-warning mb6'>
<span className='title'>
<GoAlert className='icon' />
<span className='msg'>警告检测到重复接口</span>
<span className='itf'>{interfaces[0].method} {interfaces[0].url || '-'}</span>
</span>
{interfaces.map(itf =>
<Link key={itf.id} to={URI(uri).setSearch('mod', itf.mod.id).setSearch('itf', itf.id).href()} className='mr12'>{itf.name}</Link>
)}
</div>
)}
</div>
)
}
}
export default DuplicatedInterfacesWarning

@ -0,0 +1,84 @@
import React, { Component } from 'react'
import { Link, StoreStateRouterLocationURI } from '../../family'
import { GoAlert } from 'react-icons/go'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { RouterState } from 'connected-react-router'
type DuplicatedInterfacesWarningState = any
type DuplicatedInterfacesWarningProps = {
repository: any
router: RouterState
}
class DuplicatedInterfacesWarning extends Component<DuplicatedInterfacesWarningProps, DuplicatedInterfacesWarningState> {
static contextTypes = {
store: PropTypes.object,
}
static parseDuplicatedInterfaces(repository: any) {
const counter: any = {}
for (const mod of repository.modules) {
for (const itf of mod.interfaces) {
const key = `${itf.method} ${itf.url}`
if (!counter[key]) { counter[key] = [] }
counter[key] = [...counter[key], { ...itf, mod }]
}
}
const duplicated: any[] = []
for (const key in counter) {
if (counter[key].length > 1) {
duplicated.push(counter[key])
}
}
return duplicated
}
static printDuplicatedInterfacesWarning(duplicated: any) {
duplicated.forEach((interfaces: any) => {
const key = `${interfaces[0].method} ${interfaces[0].url}`
console.group('警告:检测到重复接口 ' + key)
interfaces.forEach((itf: any) => {
console.warn(`#${itf.id} ${itf.method} ${itf.url}`)
})
console.groupEnd()
})
}
render() {
const { repository, router } = this.props
if (!repository) { return null }
const duplicated = DuplicatedInterfacesWarning.parseDuplicatedInterfaces(repository)
if (!duplicated.length) { return null }
const uri = StoreStateRouterLocationURI(router)
.removeSearch('page')
.removeSearch('itf')
return (
<div className="DuplicatedInterfacesWarning">
{duplicated.map((interfaces, index) => (
<div key={index} className="alert alert-warning mb6">
<span className="title">
<GoAlert className="icon" />
<span className="msg"></span>
<span className="itf">
{interfaces[0].method} {interfaces[0].url || '-'}
</span>
</span>
{interfaces.map((itf: any) => (
<Link
key={itf.id}
to={uri
.setSearch('mod', itf.mod.id)
.setSearch('itf', itf.id)
.href()}
className="mr12"
>
{itf.name}
</Link>
))}
</div>
))}
</div>
)
}
}
export default connect((state: any) => ({
router: state.router,
}))(DuplicatedInterfacesWarning)

@ -1,143 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, Mock, _ } from '../../family'
import { RCodeMirror } from '../utils/'
import { addProperty } from '../../actions/property'
const mockResult = process.env.NODE_ENV === 'development'
? () => ({
foo: {
bar: {
faz: {}
}
}
})
: () => ({})
class Importer extends Component {
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
handleAddMemoryProperties: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
result: JSON.stringify(mockResult(), null, 2)
}
}
render() {
const { rmodal } = this.context
return (
<section className='Importer'>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
<Link to='' onClick={e => this.handleBeautify(e)}>格式化</Link>
</div>
<form className='form-horizontal w600' onSubmit={this.handleSubmit} >
<div className='rmodal-body'>
<div className='form-group'>
{/* <SmartTextarea name='result' value={this.state.result} onChange={e => this.setState({ result: e.target.value })} className='form-control result' placeholder='Result' rows='20' /> */}
{/* TODO 2.1 完善编辑器,完善什么? */}
<RCodeMirror value={this.state.result} onChange={value => this.setState({ result: value })} ref={$rcm => { this.$rcm = $rcm }} />
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group mb0'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
{/* 这里不应该用 Link应该用 <a> 或者 fake-link */}
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
// DONE 2.1
handleBeautify = (e) => {
e.preventDefault()
if (this.$rcm) {
let result = eval('(' + this.state.result + ')') // eslint-disable-line no-eval
let beautified = JSON.stringify(result, null, 2)
this.$rcm.cm.setValue(beautified)
}
}
// TODO 2.1
// DONE 2.1 BUG Number ''
handleJSONSchema = (schema, parent = { id: -1 }, memoryProperties) => {
if (!schema) return
let { auth, repository, mod, itf, scope } = this.props
// DONE 2.1 Mock rule.type
let type = schema.type[0].toUpperCase() + schema.type.slice(1)
let rule = ''
if (type === 'Array' && schema.items && schema.items.length > 1) {
rule = schema.items.length
}
let value = /Array|Object/.test(type) ? '' : schema.template
if (schema.items && schema.items.length) {
let childType = schema.items[0].type
if (['number', 'null', 'undefined', 'boolean', 'string'].indexOf(childType) > -1) {
value = JSON.stringify(schema.template)
rule = ''
}
}
let property = Object.assign({
name: schema.name,
type,
rule,
value,
descripton: ''
}, {
creator: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
scope,
parentId: parent.id
}, {
memory: true,
id: _.uniqueId('memory-')
})
memoryProperties.push(property)
if (schema.properties) {
schema.properties.forEach(item => this.handleJSONSchema(item, property, memoryProperties))
}
if (schema.items && schema.items[0] && schema.items[0].properties) {
schema.items[0].properties.forEach(item => this.handleJSONSchema(item, property, memoryProperties))
}
}
// DONE 2.1 setState() handleAddMemoryProperty()
handleSubmit = (e) => {
e.preventDefault()
let result = eval('(' + this.state.result + ')') // eslint-disable-line no-eval
if (result instanceof Array) {
result = { _root_: result }
}
let schema = Mock.toJSONSchema(result)
let memoryProperties = []
if (schema.properties) schema.properties.forEach(item => this.handleJSONSchema(item, undefined, memoryProperties))
let { handleAddMemoryProperties } = this.context
handleAddMemoryProperties(memoryProperties, () => {
// done
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({
onAddProperty: addProperty
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Importer)

@ -0,0 +1,213 @@
import React, { Component } from 'react'
import { connect, Link, Mock, _, PropTypes } from '../../family'
import { RCodeMirror } from '../utils/'
import { addProperty } from '../../actions/property'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
const mockResult =
process.env.NODE_ENV === 'development'
? () => ({
foo: {
bar: {
faz: {},
},
},
})
: () => ({})
function isPrimitiveType(type: string) {
return ['number', 'null', 'undefined', 'boolean', 'string'].indexOf(type.toLowerCase()) > -1
}
const isIncreamentNumberSequence = (numbers: any) =>
numbers.every((num: any) => typeof num === 'number' && ((num: any, i: number) => i === 0 || num - numbers[i - 1] === 1))
function mixItemsProperties(items: any) {
// 合并 item properties 的 key返回的 item 拥有导入 json 的所有 key
if (!items || !items.length) {
return {
properties: [],
}
} else if (items.length === 1) {
if (!items[0].properties) {
items[0].properties = []
}
return items[0]
} else {
const baseItem = items[0]
if (!baseItem.properties) {
baseItem.properties = []
}
const baseProperties = baseItem.properties
for (let i = 1; i < items.length; ++i) {
const item = items[i]
if (item.properties && item.properties.length) {
for (const p of item.properties) {
if (!baseProperties.find((e: any) => e.name === p.name)) {
baseProperties.push(p)
}
}
}
}
return baseItem
}
}
type ImporterProps = {
rmodal?: any,
title?: any,
handleAddMemoryProperties: (...args: any[]) => any,
[k: string]: any;
}
type ImporterState = {
result: string
}
class Importer extends Component<ImporterProps, ImporterState> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
handleAddMemoryProperties: PropTypes.func.isRequired,
}
$rcm: any
constructor(props: any) {
super(props)
this.state = {
result: JSON.stringify(mockResult(), null, 2),
}
}
render() {
const { rmodal } = this.context
return (
<section className="Importer">
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
<Link to="" onClick={e => this.handleBeautify(e)}>
</Link>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group">
<RCodeMirror
value={this.state.result}
onChange={(value: any) => this.setState({ result: value })}
ref={$rcm => {
this.$rcm = $rcm
}}
/>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group mb0">
<Button type="submit" style={{ marginRight: 8 }} variant="contained" color="primary">
</Button>
<Button onClick={() => rmodal.close()} > </Button>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
// DONE 2.1 支持格式化
handleBeautify = (e: any) => {
e.preventDefault()
if (this.$rcm) {
const result = eval('(' + this.state.result + ')') // eslint-disable-line no-eval
const beautified = JSON.stringify(result, null, 2)
this.$rcm.cm.setValue(beautified)
}
};
// TODO 2.1 待完整测试各种输入
// DONE 2.1 BUG 类型 Number初始值 '',被解析为随机字符串
handleJSONSchema = (schema: any, parent = { id: '-1' }, memoryProperties: any, siblings?: any) => {
if (!schema) { return }
const { auth, repository, mod, itf, scope } = this.props
const hasSiblings = siblings instanceof Array && siblings.length > 0
// DONE 2.1 需要与 Mock 的 rule.type 规则统一,首字符小写,好烦!应该忽略大小写!
let type = schema.type[0].toUpperCase() + schema.type.slice(1)
let rule = ''
if (type === 'Array' && schema.items && schema.items.length > 1) {
rule = schema.items.length
}
let value = /Array|Object/.test(type) ? '' : schema.template
if (schema.items && schema.items.length) {
const childType = schema.items[0].type
if (isPrimitiveType(childType)) {
value = JSON.stringify(schema.template)
rule = ''
}
} else if (hasSiblings && isPrimitiveType(type)) {
// 如果是简单数据可以在这里进行合并
const valueArr = siblings.map((s: any) => s && s.template)
if (_.uniq(valueArr).length > 1) {
// 只有在数组里有不同元素时再合并
if (isIncreamentNumberSequence(valueArr)) {
// 如果是递增数字序列特殊处理
value = valueArr[0]
rule = '+1'
} else {
// 比如 [{a:1},{a:2}]
// 我们可以用 type: Array rule: +1 value: [1,2] 进行还原
value = JSON.stringify(valueArr)
type = 'Array'
rule = '+1'
}
}
}
const property = Object.assign(
{
name: schema.name,
type,
rule,
value,
descripton: '',
},
{
creator: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
scope,
parentId: parent.id,
},
{
memory: true,
id: _.uniqueId('memory-'),
}
)
memoryProperties.push(property)
if (schema.properties) {
schema.properties.forEach((item: any) => {
const childSiblings = hasSiblings ? siblings.map((s: any) => s && (s.properties.find((p: any) => p.name === item.name) || null)) : undefined
this.handleJSONSchema(item, property, memoryProperties, childSiblings)
})
}
mixItemsProperties(schema.items).properties.forEach((item: any) => {
const siblings = schema.items.map((o: any) => o.properties.find((p: any) => p.name === item.name) || null)
this.handleJSONSchema(item, property, memoryProperties, siblings)
})
};
// DONE 2.1 因为 setState() 是异步的,导致重复调用 handleAddMemoryProperty() 时最后保留最后一个临时属性
handleSubmit = (e: any) => {
e.preventDefault()
let result = eval('(' + this.state.result + ')') // eslint-disable-line no-eval
if (result instanceof Array) {
result = { _root_: result }
}
const schema = Mock.toJSONSchema(result)
const memoryProperties: any = []
if (schema.properties) { schema.properties.forEach((item: any) => this.handleJSONSchema(item, undefined, memoryProperties)) }
const { handleAddMemoryProperties } = this.context
handleAddMemoryProperties(memoryProperties, () => {
// done
const { rmodal } = this.context
if (rmodal) { rmodal.resolve() }
})
};
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
})
const mapDispatchToProps = {
onAddProperty: addProperty,
}
export default connect(mapStateToProps, mapDispatchToProps)(Importer)

@ -1,196 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, _ } from '../../family'
import InterfaceEditorToolbar from './InterfaceEditorToolbar'
import InterfaceSummary, { BODY_OPTION, REQUEST_PARAMS_TYPE, rptFromStr2Num } from './InterfaceSummary'
import PropertyList from './PropertyList'
import { RModal } from '../utils'
import MoveInterfaceForm from './MoveInterfaceForm'
import { fetchRepository } from '../../actions/repository'
export const RequestPropertyList = (props) => {
return <PropertyList scope='request' title='请求参数' label='请求' {...props} />
}
export const ResponsePropertyList = (props) => (
<PropertyList scope='response' title='响应内容' label='响应' {...props} />
)
// TODO 2.x MySQL Workbench
// TODO 2.x
class InterfaceEditor extends Component {
static propTypes = {
auth: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
properties: PropTypes.array.isRequired,
mod: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired
}
static contextTypes = {
store: PropTypes.object.isRequired,
onLockInterface: PropTypes.func.isRequired,
onUnlockInterface: PropTypes.func.isRequired,
onUpdateProperties: PropTypes.func.isRequired
}
static childContextTypes = {
handleLockInterface: PropTypes.func.isRequired,
handleUnlockInterface: PropTypes.func.isRequired,
handleSaveInterface: PropTypes.func.isRequired,
handleMoveInterface: PropTypes.func.isRequired,
handleAddMemoryProperty: PropTypes.func.isRequired,
handleAddMemoryProperties: PropTypes.func.isRequired,
handleDeleteMemoryProperty: PropTypes.func.isRequired,
handleChangeProperty: PropTypes.func.isRequired
}
constructor (props) {
super(props)
this.state = {
...InterfaceEditor.mapPropsToState(props),
summaryState: {
bodyOption: BODY_OPTION.FORM_DATA,
requestParamsType: REQUEST_PARAMS_TYPE.QUERY_PARAMS
},
moveInterfaceDialogOpen: false
}
this.summaryStateChange = this.summaryStateChange.bind(this)
// { itf: {}, properties: [] }
}
getChildContext () {
return _.pick(this, Object.keys(InterfaceEditor.childContextTypes))
}
static mapPropsToState (prevProps, prevStates) {
let { auth, itf, properties } = prevProps
return {
...prevStates,
itf,
properties: properties.map(property => ({ ...property })),
editable: !!(itf.locker && (itf.locker.id === auth.id))
}
}
summaryStateChange (summaryState) {
this.setState({ summaryState })
}
componentWillReceiveProps (nextProps) {
if (
nextProps.itf.id === this.state.itf.id &&
nextProps.itf.updatedAt === this.state.itf.updatedAt
) return
const prevStates = this.state
this.setState(InterfaceEditor.mapPropsToState(nextProps, prevStates))
}
// Use shouldComponentUpdate() to let React know if a component's output is not affected by the current change in state or props.
// TODO 2.2
// shouldComponentUpdate (nextProps, nextState) {}
render () {
const { auth, repository, mod, itf } = this.props
const { editable } = this.state
const { id, locker } = this.state.itf
if (!id) return null
return (
<article className='InterfaceEditor'>
<InterfaceEditorToolbar locker={locker} auth={auth} repository={repository} editable={editable} />
<InterfaceSummary repository={repository} mod={mod} itf={itf} active editable={editable} stateChangeHandler={this.summaryStateChange} />
<RequestPropertyList
properties={this.state.properties}
editable={editable}
repository={repository}
mod={mod}
itf={this.state.itf}
bodyOption={this.state.summaryState.bodyOption}
requestParamsType={this.state.summaryState.requestParamsType}
/>
<ResponsePropertyList properties={this.state.properties} editable={editable}
repository={repository} mod={mod} itf={this.state.itf} />
<RModal
when={this.state.moveInterfaceDialogOpen}
onClose={e => this.setState({ moveInterfaceDialogOpen: false })}
onResolve={this.handleMoveInterfaceSubmit}
>
<MoveInterfaceForm title='移动接口' repository={repository} itfId={itf.id} />
</RModal>
</article>
)
}
handleAddMemoryProperty = (property, cb) => {
this.handleAddMemoryProperties([property], cb)
}
handleAddMemoryProperties = (properties, cb) => {
const requestParamsType = this.state.summaryState.requestParamsType
const rpt = rptFromStr2Num(requestParamsType)
properties.forEach(item => {
if (item.memory === undefined) item.memory = true
if (item.id === undefined) item.id = _.uniqueId('memory-')
item.pos = rpt
})
let nextState = { properties: [...this.state.properties, ...properties] }
this.setState(nextState, () => {
if (cb) cb(properties)
})
}
handleDeleteMemoryProperty = (property, cb) => {
let properties = [...this.state.properties]
let index = properties.findIndex(item => item.id === property.id)
if (index >= 0) {
properties.splice(index, 1)
//
let deletedParentIds = [property.id]
for (let index = 0; index < properties.length; index++) {
if (deletedParentIds.indexOf(properties[index].parentId) !== -1) {
deletedParentIds.push(properties[index].id)
properties.splice(index--, 1)
index = 0 //
}
}
this.setState({ properties }, () => {
if (cb) cb()
})
}
}
handleChangeProperty = (property) => {
let properties = [...this.state.properties]
let index = properties.findIndex(item => item.id === property.id)
if (index >= 0) {
properties.splice(index, 1, property)
this.setState({ properties }, () => { })
}
}
handleSaveInterface = (e) => {
e.preventDefault()
const { onUpdateProperties } = this.context
onUpdateProperties(this.state.itf.id, this.state.properties, this.state.summaryState, () => {
this.handleUnlockInterface()
})
}
handleMoveInterface = (e) => {
e.preventDefault()
this.setState({
moveInterfaceDialogOpen: true
})
}
handleMoveInterfaceSubmit = () => {
}
handleLockInterface = () => {
let { onLockInterface } = this.context
let { itf } = this.props
onLockInterface(itf.id)
}
handleUnlockInterface = () => {
let { onUnlockInterface } = this.context
let { itf } = this.props
onUnlockInterface(itf.id)
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
fetchRepository,
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceEditor)

@ -0,0 +1,232 @@
import React, { Component } from 'react'
import { PropTypes, connect, _ } from '../../family'
import InterfaceEditorToolbar from './InterfaceEditorToolbar'
import InterfaceSummary, { BODY_OPTION, REQUEST_PARAMS_TYPE, rptFromStr2Num } from './InterfaceSummary'
import PropertyList from './PropertyList'
import { RModal } from '../utils'
import MoveInterfaceForm from './MoveInterfaceForm'
import { fetchRepository } from '../../actions/repository'
import { RootState } from 'actions/types'
import { lockInterface, unlockInterface } from 'actions/interface'
import { updateProperties } from 'actions/property'
export const RequestPropertyList = (props: any) => {
return <PropertyList scope="request" title="请求参数" label="请求" {...props} />
}
export const ResponsePropertyList = (props: any) => (
<PropertyList scope="response" title="响应内容" label="响应" {...props} />
)
type InterfaceEditorProps = {
auth: any
itf: any
properties: any[]
mod: any
repository: any
lockInterface: typeof lockInterface
unlockInterface: typeof unlockInterface
updateProperties: typeof updateProperties
}
type InterfaceEditorState = {
summaryState: any
itf: any
properties: any
editable: boolean
moveInterfaceDialogOpen: boolean,
}
// TODO 2.x 参考 MySQL Workbench 的字段编辑器
// TODO 2.x 支持复制整个接口到其他模块、其他项目
class InterfaceEditor extends Component<
InterfaceEditorProps,
InterfaceEditorState
> {
static childContextTypes = {
handleLockInterface: PropTypes.func.isRequired,
handleUnlockInterface: PropTypes.func.isRequired,
handleSaveInterface: PropTypes.func.isRequired,
handleMoveInterface: PropTypes.func.isRequired,
handleAddMemoryProperty: PropTypes.func.isRequired,
handleAddMemoryProperties: PropTypes.func.isRequired,
handleDeleteMemoryProperty: PropTypes.func.isRequired,
handleChangeProperty: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = {
...InterfaceEditor.mapPropsToState(props),
summaryState: {
bodyOption: BODY_OPTION.FORM_DATA,
requestParamsType: REQUEST_PARAMS_TYPE.QUERY_PARAMS,
},
moveInterfaceDialogOpen: false,
}
this.summaryStateChange = this.summaryStateChange.bind(this)
// { itf: {}, properties: [] }
}
static mapPropsToState(prevProps: any, prevStates: any = {}) {
const { auth, itf, properties } = prevProps
return {
...prevStates,
itf,
properties: properties.map((property: any) => ({ ...property })),
editable: !!(itf.locker && (itf.locker.id === auth.id)),
}
}
getChildContext() {
return _.pick(this, Object.keys(InterfaceEditor.childContextTypes))
}
summaryStateChange(summaryState: any) {
this.setState({ summaryState })
}
componentWillReceiveProps(nextProps: any) {
if (
nextProps.itf.id === this.state.itf.id &&
nextProps.itf.updatedAt === this.state.itf.updatedAt
) { return }
const prevStates = this.state
this.setState(InterfaceEditor.mapPropsToState(nextProps, prevStates))
}
// Use shouldComponentUpdate() to let React know if a component's output is not affected by the current change in state or props.
// TODO 2.2
// shouldComponentUpdate (nextProps, nextState) {}
render() {
const { auth, repository, mod, itf } = this.props
const { editable } = this.state
const { id, locker } = this.state.itf
if (!id) { return null }
return (
<article className="InterfaceEditor">
<InterfaceEditorToolbar
locker={locker}
auth={auth}
repository={repository}
editable={editable}
itfId={itf.id}
moveInterface={this.handleMoveInterface}
handleLockInterface={this.handleLockInterface}
handleMoveInterface={this.handleMoveInterface}
handleSaveInterface={this.handleSaveInterface}
handleUnlockInterface={this.handleUnlockInterface}
/>
<InterfaceSummary
repository={repository}
mod={mod}
itf={itf}
active={true}
editable={editable}
stateChangeHandler={this.summaryStateChange}
/>
<RequestPropertyList
properties={this.state.properties}
editable={editable}
repository={repository}
mod={mod}
itf={this.state.itf}
bodyOption={this.state.summaryState.bodyOption}
requestParamsType={this.state.summaryState.requestParamsType}
/>
<ResponsePropertyList
properties={this.state.properties}
editable={editable}
repository={repository}
mod={mod}
itf={this.state.itf}
/>
<RModal
when={this.state.moveInterfaceDialogOpen}
onClose={() => this.setState({ moveInterfaceDialogOpen: false })}
onResolve={this.handleMoveInterfaceSubmit}
>
<MoveInterfaceForm title="移动接口" repository={repository} itfId={itf.id} />
</RModal>
</article>
)
}
handleAddMemoryProperty = (property: any, cb: any) => {
this.handleAddMemoryProperties([property], cb)
}
handleAddMemoryProperties = (properties: any, cb: any) => {
const requestParamsType = this.state.summaryState.requestParamsType
const rpt = rptFromStr2Num(requestParamsType)
properties.forEach((item: any) => {
if (item.memory === undefined) { item.memory = true }
if (item.id === undefined) { item.id = _.uniqueId('memory-') }
item.pos = rpt
})
const nextState = { properties: [...this.state.properties, ...properties] }
this.setState(nextState, () => {
if (cb) { cb(properties) }
})
}
handleDeleteMemoryProperty = (property: any, cb: any) => {
const properties = [...this.state.properties]
const index = properties.findIndex(item => item.id === property.id)
if (index >= 0) {
properties.splice(index, 1)
// 清除后代属性
const deletedParentIds = [property.id]
for (let index = 0; index < properties.length; index++) {
if (deletedParentIds.indexOf(properties[index].parentId) !== -1) {
deletedParentIds.push(properties[index].id)
properties.splice(index--, 1)
index = 0 // 强制从头开始查找,避免漏掉后代属性
}
}
this.setState({ properties }, () => {
if (cb) { cb() }
})
}
}
handleChangeProperty = (property: any) => {
const properties = [...this.state.properties]
const index = properties.findIndex(item => item.id === property.id)
if (index >= 0) {
properties.splice(index, 1, property)
this.setState({ properties })
}
}
handleSaveInterface = (e: any) => {
e.preventDefault()
const { updateProperties } = this.props
updateProperties(this.state.itf.id, this.state.properties, this.state.summaryState, () => {
this.handleUnlockInterface()
})
}
handleMoveInterface = () => {
this.setState({
moveInterfaceDialogOpen: true,
})
}
handleMoveInterfaceSubmit = () => {
/** empty */
}
handleLockInterface = () => {
const { itf, lockInterface } = this.props
lockInterface(itf.id)
}
handleUnlockInterface = () => {
const { itf, unlockInterface } = this.props
unlockInterface(itf.id)
}
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
fetchRepository,
})
const mapDispatchToProps = {
lockInterface,
unlockInterface,
updateProperties,
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceEditor)

@ -1,57 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { GoPencil, GoGitPullRequest, GoX, GoMoveRight } from 'react-icons/lib/go'
// TODO 2.1 BUG
class InterfaceEditorToolbar extends Component {
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
locker: PropTypes.object,
editable: PropTypes.bool.isRequired
}
static contextTypes = {
handleLockInterface: PropTypes.func.isRequired,
handleUnlockInterface: PropTypes.func.isRequired,
handleSaveInterface: PropTypes.func.isRequired,
handleMoveInterface: PropTypes.func.isRequired
}
render () {
let { handleLockInterface, handleUnlockInterface, handleSaveInterface, handleMoveInterface } = this.context
let { editable, locker, auth, repository } = this.props
let isOwned = repository.owner.id === auth.id
let isJoined = repository.members.find(itme => itme.id === auth.id)
if (!isOwned && !isJoined) return null
if (editable) {
return (
<div className='InterfaceEditorToolbar'>
<button className='btn btn-success save w130' onClick={handleSaveInterface}><GoGitPullRequest /> 保存</button>
<button className='btn btn-default cancel w130' onClick={handleUnlockInterface}><GoX /> 取消</button>
<span className='locker-warning hide'>已经锁定当前接口</span>
{/* 这个提示让界面有点混乱,暂时隐藏掉 */}
{/* .locker-success .locker-success */}
</div>
)
}
if (locker) {
return (
<div className='InterfaceEditorToolbar'>
<div className='alert alert-danger'>当前接口已经被 <span className='nowrap'>{locker.fullname}</span> 锁定</div>
</div>
)
}
return (
<div className='InterfaceEditorToolbar'>
<button className='btn btn-secondary edit w130' onClick={handleMoveInterface}><GoMoveRight /> 移动</button>
<button className='btn btn-success edit w130' onClick={handleLockInterface}><GoPencil /> 编辑</button>
</div>
)
}
}
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceEditorToolbar)

@ -0,0 +1,84 @@
import React from 'react'
import { Button, makeStyles, Theme, createStyles } from '@material-ui/core'
import LoadingButton from '../common/LoadingButton'
import Create from '@material-ui/icons/Create'
import KeyboardTab from '@material-ui/icons/KeyboardTab'
import Save from '@material-ui/icons/Save'
import Cancel from '@material-ui/icons/Cancel'
import { useSelector } from 'react-redux'
import { RootState } from 'actions/types'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
button: {
margin: theme.spacing(1),
},
leftIcon: {
marginRight: theme.spacing(1),
},
rightIcon: {
marginLeft: theme.spacing(1),
},
iconSmall: {
fontSize: 20,
},
})
)
interface Props {
auth: any,
repository: any,
locker?: any,
editable: boolean,
itfId: number,
moveInterface: any
handleSaveInterface: any
handleUnlockInterface: any
handleMoveInterface: any
handleLockInterface: any
}
function InterfaceEditorToolbar(props: Props) {
const { editable, locker, auth, repository, handleLockInterface, handleMoveInterface,
handleSaveInterface, handleUnlockInterface } = props
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((item: any) => item.id === auth.id)
const loading = useSelector((state: RootState) => state.loading)
const classes = useStyles()
if (!isOwned && !isJoined) { return null }
if (editable) {
return (
<div className="InterfaceEditorToolbar">
<LoadingButton className={classes.button} onClick={handleSaveInterface} variant="contained" color="primary" disabled={loading} label="保存">
<Save className={classes.rightIcon} />
</LoadingButton>
<Button className={classes.button} onClick={handleUnlockInterface} variant="contained">
<Cancel className={classes.rightIcon} />
</Button>
<span className="locker-warning hide"></span>
</div>
)
}
if (locker) {
return (
<div className="InterfaceEditorToolbar">
<div className="alert alert-danger"> <span className="nowrap">{locker.fullname}</span> </div>
</div>
)
}
return (
<div className="InterfaceEditorToolbar">
<Button className={classes.button} onClick={handleMoveInterface} variant="contained">
/
<KeyboardTab className={classes.rightIcon} />
</Button>
<LoadingButton className={classes.button} onClick={handleLockInterface} variant="contained" color="primary" disabled={loading} label="编辑">
<Create className={classes.rightIcon} />
</LoadingButton>
</div>
)
}
export default InterfaceEditorToolbar

@ -1,135 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, Mock } from '../../family'
import { SmartTextarea } from '../utils'
export const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'HEAD']
export const STATUS_LIST = [200, 301, 403, 404, 500, 502, 503, 504]
//
const mockInterface = process.env.NODE_ENV === 'development'
? () => Mock.mock({
name: '接口@CTITLE(4)',
url: '@URL',
'method|1': METHODS,
description: '@CPARAGRAPH',
repositoryId: undefined,
moduleId: undefined
})
: () => ({
name: '',
url: '',
method: 'GET',
description: '',
repositoryId: undefined,
moduleId: undefined
})
class InterfaceForm extends Component {
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
onAddInterface: PropTypes.func.isRequired,
onUpdateInterface: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object,
title: PropTypes.string.isRequired
}
constructor (props) {
super(props)
let itf = this.props.itf
this.state = itf ? { ...itf } : mockInterface()
}
render () {
const { rmodal } = this.context
return (
<section>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<form className='form-horizontal w600' onSubmit={this.handleSubmit} >
<div className='rmodal-body'>
<div className='form-group row'>
<label className='col-sm-2 control-label'>名称</label>
<div className='col-sm-10'>
<input name='name' tabIndex={1} value={this.state.name} onChange={e => this.setState({ name: e.target.value })} className='form-control' placeholder='Name' spellCheck='false' autoFocus='true' required />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>地址</label>
<div className='col-sm-10'>
<input name='name' tabIndex={2} value={this.state.url} onChange={e => this.setState({ url: e.target.value })} className='form-control' placeholder='URI' spellCheck='false' required />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>类型</label>
<div className='col-sm-10'>
<select name='method' tabIndex={3} value={this.state.method} onChange={e => this.setState({ method: e.target.value })} className='form-control'>
{METHODS.map(method =>
<option key={method} value={method}>{method}</option>
)}
</select>
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>状态码</label>
<div className='col-sm-10'>
<select name='status' tabIndex={4} value={this.state.status} onChange={e => this.setState({ status: e.target.value })} className='form-control'>
{STATUS_LIST.map(status =>
<option key={status} value={status}>{status}</option>
)}
</select>
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>简介</label>
<div className='col-sm-10'>
<SmartTextarea name='description' tabIndex={5} value={this.state.description} onChange={e => this.setState({ description: e.target.value })} className='form-control' placeholder='Description' spellCheck='false' rows='5' />
</div>
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate () {
this.context.rmodal.reposition()
}
handleSubmit = (e) => {
e.preventDefault()
let { onAddInterface, onUpdateInterface } = this.context
let { auth, repository, mod } = this.props
let onAddOrUpdateInterface = this.state.id ? onUpdateInterface : onAddInterface
let itf = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
lockerId: this.state.locker ? this.state.locker.id : null
})
onAddOrUpdateInterface(itf, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceForm)

@ -0,0 +1,173 @@
import React, { Component } from 'react'
import { PropTypes, connect, Mock } from '../../family'
import { SmartTextarea } from '../utils'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
export const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'HEAD']
export const STATUS_LIST = [200, 301, 403, 404, 500, 502, 503, 504]
// 模拟数据
const mockInterface =
process.env.NODE_ENV === 'development'
? () =>
Mock.mock({
name: '接口@CTITLE(4)',
url: '@URL',
'method|1': METHODS,
description: '@CPARAGRAPH',
repositoryId: undefined,
moduleId: undefined,
})
: () => ({
name: '',
url: '',
method: 'GET',
description: '',
repositoryId: undefined,
moduleId: undefined,
})
type InterfaceFormProps = any
type InterfaceFormState = any
class InterfaceForm extends Component<InterfaceFormProps, InterfaceFormState> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddInterface: PropTypes.func.isRequired,
onUpdateInterface: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
const itf = this.props.itf
this.state = itf ? { ...itf } : mockInterface()
}
render() {
const { rmodal } = this.context
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={1}
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoFocus={true}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={2}
value={this.state.url}
onChange={e => this.setState({ url: e.target.value })}
className="form-control"
placeholder="URI"
spellCheck={false}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<select
name="method"
tabIndex={3}
value={this.state.method}
onChange={e => this.setState({ method: e.target.value })}
className="form-control"
>
{METHODS.map(method => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<select
name="status"
tabIndex={4}
value={this.state.status}
onChange={e => this.setState({ status: e.target.value })}
className="form-control"
>
{STATUS_LIST.map(status => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<SmartTextarea
name="description"
tabIndex={5}
value={this.state.description}
onChange={(e: any) => this.setState({ description: e.target.value })}
className="form-control"
placeholder="Description"
spellCheck={false}
rows="5"
/>
</div>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" variant="contained" color="primary" style={{marginRight: 8}}>
</Button>
<Button onClick={() => rmodal.close()} > </Button>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
handleSubmit = (e: any) => {
e.preventDefault()
const { onAddInterface, onUpdateInterface } = this.context
const { auth, repository, mod } = this.props
const onAddOrUpdateInterface = this.state.id ? onUpdateInterface : onAddInterface
const itf = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
lockerId: this.state.locker ? this.state.locker.id : null,
})
onAddOrUpdateInterface(itf, () => {
const { rmodal } = this.context
if (rmodal) { rmodal.resolve() }
})
};
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
})
const mapDispatchToProps = {}
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceForm)

@ -1,147 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, replace, StoreStateRouterLocationURI } from '../../family'
import { RModal, RSortable } from '../utils'
import InterfaceForm from './InterfaceForm'
import { GoPencil, GoTrashcan, GoRocket, GoLock } from 'react-icons/lib/go'
import { getCurrentInterface } from '../../selectors/interface';
import './InterfaceList.css'
class Interface extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
onDeleteInterface: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
active: PropTypes.bool.isRequired,
curItf: PropTypes.object,
}
constructor(props) {
super(props)
this.state = { update: false }
}
render() {
let { store } = this.context
let { auth, repository, mod, itf } = this.props
let selectHref = StoreStateRouterLocationURI(store).setSearch('itf', itf.id).href()
let isOwned = repository.owner.id === auth.id
let isJoined = repository.members.find(itme => itme.id === auth.id)
return (
<div className='Interface clearfix'>
{/* 这层 name 包裹的有点奇怪name 应该直接加到 a 上 */}
{/* TODO 2.3 <a> 的范围应该扩大至整个 Interface否则只有点击到 <a> 才能切换,现在不容易点击到 <a> */}
<span className='name'>
{itf.locker ? <span className='locked mr5'><GoLock /></span> : null}
<Link to={selectHref} onClick={(e) => {
if (this.props.curItf && this.props.curItf.locker && !window.confirm('编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?')) {
e.preventDefault()
}
}}><span>{itf.name}</span></Link>
</span>
{isOwned || isJoined
? <div className='toolbar'>
{/* DONE 2.2 X 支持双击修改 */}
{!itf.locker || itf.locker.id === auth.id
? <span className='fake-link' onClick={e => this.setState({ update: true })}><GoPencil /></span>
: null
}
<RModal when={this.state.update} onClose={e => this.setState({ update: false })} onResolve={this.handleUpdate}>
<InterfaceForm title='修改接口' repository={repository} mod={mod} itf={itf} />
</RModal>
{!itf.locker
? <Link to='' onClick={e => this.handleDelete(e, itf)}><GoTrashcan /></Link>
: null
}
</div>
: null
}
</div>
)
}
handleDelete = (e, itf) => {
e.preventDefault()
let message = `接口被删除后不可恢复!\n确认继续删除『#${itf.id} ${itf.name}』吗?`
if (window.confirm(message)) {
let { onDeleteInterface } = this.context
onDeleteInterface(itf.id, () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
let deleteHref = this.props.active ? uri.removeSearch('itf').href() : uri.href()
store.dispatch(replace(deleteHref))
})
}
}
handleUpdate = (e) => {
}
}
class InterfaceList extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
onSortInterfaceList: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itfs: PropTypes.array,
itf: PropTypes.object,
curItf: PropTypes.object,
}
constructor(props) {
super(props)
this.state = { create: false }
}
render() {
let { auth, repository, mod, itfs = [], itf, curItf } = this.props
if (!mod.id) return null
let isOwned = repository.owner.id === auth.id
let isJoined = repository.members.find(itme => itme.id === auth.id)
return (
<article className='InterfaceList'>
<RSortable onChange={this.handleSort} disabled={!isOwned && !isJoined}>
<ul className='body'>
{itfs.map(item =>
<li key={item.id} className={item.id === itf.id ? 'active sortable' : 'sortable'} data-id={item.id}>
<Interface repository={repository} mod={mod} itf={item} active={item.id === itf.id} auth={auth} curItf={curItf} />
</li>
)}
</ul>
</RSortable>
{isOwned || isJoined
? <div className='footer'>
{/* DONE 2.2 反复 setState() 还是很繁琐,需要提取一个类似 DialogController 的组件 */}
{/* DONE 2.2 如何重构为高阶组件 */}
<span className='fake-link' onClick={e => this.setState({ create: true })}>
<span className='fontsize-14'><GoRocket /></span> 新建接口
</span>
<RModal when={this.state.create} onClose={e => this.setState({ create: false })} onResolve={this.handleCreate}>
<InterfaceForm title='新建接口' repository={repository} mod={mod} />
</RModal>
</div>
: null}
</article>
)
}
handleCreate = (e) => {
}
handleSort = (e, sortable) => {
let { onSortInterfaceList } = this.context
onSortInterfaceList(sortable.toArray())
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
curItf: getCurrentInterface(state),
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceList)

@ -0,0 +1,140 @@
import React, { Component } from 'react'
import { connect, Link, replace, StoreStateRouterLocationURI } from '../../family'
import { RModal, RSortable } from '../utils'
import InterfaceForm from './InterfaceForm'
import { GoPencil, GoTrashcan, GoRocket, GoLock } from 'react-icons/go'
import { getCurrentInterface } from '../../selectors/interface'
import PropTypes from 'prop-types'
import './InterfaceList.css'
import { RootState } from 'actions/types'
type InterfaceProps = any
type InterfaceState = any
class InterfaceBase extends Component<InterfaceProps, InterfaceState> {
static contextTypes = {
store: PropTypes.object,
onDeleteInterface: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = { update: false }
}
render() {
const { auth, repository, mod, itf, router } = this.props
const selectHref = StoreStateRouterLocationURI(router)
.setSearch('itf', itf.id)
.href()
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((i: any) => i.id === auth.id)
return (
<div className="Interface clearfix">
<span className="name">
{itf.locker ? (
<span className="locked mr5">
<GoLock />
</span>
) : null}
<Link
to={selectHref}
onClick={e => {
if (
this.props.curItf &&
this.props.curItf.locker &&
!window.confirm('编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?')
) {
e.preventDefault()
}
}}
>
<span>{itf.name}</span>
</Link>
</span>
{isOwned || isJoined ? (
<div className="toolbar">
{!itf.locker || itf.locker.id === auth.id ? (
<span className="fake-link" onClick={() => this.setState({ update: true })}>
<GoPencil />
</span>
) : null}
<RModal when={this.state.update} onClose={() => this.setState({ update: false })} onResolve={this.handleUpdate}>
<InterfaceForm title="修改接口" repository={repository} mod={mod} itf={itf} />
</RModal>
{!itf.locker ? (
<Link to="" onClick={e => this.handleDelete(e, itf)}>
<GoTrashcan />
</Link>
) : null}
</div>
) : null}
</div>
)
}
handleDelete = (e: any, itf: any) => {
e.preventDefault()
const message = `接口被删除后不可恢复!\n确认继续删除『#${itf.id} ${itf.name}』吗?`
if (window.confirm(message)) {
const { onDeleteInterface } = this.context
onDeleteInterface(itf.id, () => {
const { store } = this.context
const uri = StoreStateRouterLocationURI(store)
const deleteHref = this.props.active ? uri.removeSearch('itf').href() : uri.href()
store.dispatch(replace(deleteHref))
})
}
};
handleUpdate = () => { /** test */ }
}
const Interface = connect((state: any) => ({ router: state.router }))(InterfaceBase)
type InterfaceListProps = any
type InterfaceListState = any
class InterfaceList extends Component<InterfaceListProps, InterfaceListState> {
constructor(props: any) {
super(props)
this.state = { create: false }
}
render() {
const { auth, repository, mod, itfs = [], itf, curItf } = this.props
if (!mod.id) { return null }
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((i: any) => i.id === auth.id)
return (
<article className="InterfaceList">
<RSortable onChange={this.handleSort} disabled={!isOwned && !isJoined}>
<ul className="body">
{itfs.map((item: any) => (
<li key={item.id} className={item.id === itf.id ? 'active sortable' : 'sortable'} data-id={item.id}>
<Interface repository={repository} mod={mod} itf={item} active={item.id === itf.id} auth={auth} curItf={curItf} />
</li>
))}
</ul>
</RSortable>
{isOwned || isJoined ? (
<div className="footer">
<span className="fake-link" onClick={() => this.setState({ create: true })}>
<span className="fontsize-14">
<GoRocket />
</span>{' '}
</span>
<RModal when={this.state.create} onClose={() => this.setState({ create: false })} onResolve={this.handleCreate}>
<InterfaceForm title="新建接口" repository={repository} mod={mod} />
</RModal>
</div>
) : null}
</article>
)
}
handleCreate = () => { /** empty */ }
handleSort = (_: any, sortable: any) => {
const { onSortInterfaceList } = this.context
onSortInterfaceList(sortable.toArray())
};
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
curItf: getCurrentInterface(state),
router: state.router,
})
const mapDispatchToProps = {}
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceList)

@ -2,96 +2,96 @@ import React, { Component } from 'react'
import { PropTypes, Link, Mock, _ } from '../../family'
import { Tree } from '../utils'
import { serve } from '../../relatives/services/constant'
import { GoLink, GoSync, GoBeer, GoBug } from 'react-icons/lib/go'
import { RE_KEY } from 'mockjs/src/mock/constant'
import { GoLink, GoSync, GoBeaker, GoBug } from 'react-icons/go'
class Previewer extends Component {
/*eslint no-useless-escape: 0*/
const RE_KEY = /(.+)\|(?:\+(\d+)|([\+\-]?\d+-?[\+\-]?\d*)?(?:\.(\d+-?\d*))?)/
class Previewer extends Component<any, any> {
static propTypes = {
label: PropTypes.string.isRequired,
scope: PropTypes.string.isRequired,
properties: PropTypes.array.isRequired,
itf: PropTypes.object.isRequired
itf: PropTypes.object.isRequired,
}
render() {
let scopedTemplate
let scopedProperties
let scopedData
let scopedData: any
let scopedKeys
let extraKeys
let { label, scope, properties, itf } = this.props
const { label, scope, properties, itf } = this.props
try {
// DONE 2.2 支持引用请求参数
scopedProperties = {
request: properties.map(property => ({ ...property })).filter(property => property.scope === 'request'),
response: properties.map(property => ({ ...property })).filter(property => property.scope === 'response')
};
request: properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === 'request'),
response: properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === 'response'),
}
scopedTemplate = {
request: Tree.treeToJson(Tree.arrayToTree(scopedProperties.request)),
response: Tree.treeToJson(Tree.arrayToTree(scopedProperties.response))
};
response: Tree.treeToJson(Tree.arrayToTree(scopedProperties.response)),
}
scopedKeys = {
request: Object.keys(scopedTemplate.request).map(item => item.replace(RE_KEY, '$1')),
response: Object.keys(scopedTemplate.response).map(item => item.replace(RE_KEY, '$1'))
response: Object.keys(scopedTemplate.response).map(item => item.replace(RE_KEY, '$1')),
}
extraKeys = _.difference(scopedKeys.request, scopedKeys.response)
scopedData = {
request: Mock.mock(scopedTemplate.request)
request: Mock.mock(scopedTemplate.request),
}
scopedData.response = Mock.mock(
Object.assign({}, _.pick(scopedData.request, extraKeys), scopedTemplate.response)
)
scopedData.response = _.pick(scopedData.response, scopedKeys.response)
let template = scopedTemplate[scope]
const template = (scopedTemplate as any)[scope]
let data = scopedData[scope]
if (data._root_) {
data = data._root_
}
// DONE 2.1 支持虚拟属性 __root__ √服务端 √前端 √迁移测试
let keys = Object.keys(data)
if (keys.length === 1 && keys[0] === '__root__') data = data.__root__
const keys = Object.keys(data)
if (keys.length === 1 && keys[0] === '__root__') { data = data.__root__ }
let { Assert } = Mock.valid
let valid = Mock.valid(template, data)
for (var i = 0; i < valid.length; i++) {
console.warn(Assert.message(valid[i]))
const { Assert } = Mock.valid
const valid = Mock.valid(template, data)
for (const i of valid) {
console.warn(Assert.message(i))
}
return (
<div className='Previewer row'>
<div className='result-template col-6'>
<div className='header'>
<span className='title'>{label}</span>
<div className="Previewer row">
<div className="result-template col-6">
<div className="header">
<span className="title">{label}</span>
{scope === 'response'
? <a href={`${serve}/app/mock/template/${itf.id}`} target='_blank'><GoLink className='fontsize-14' /></a>
? <a href={`${serve}/app/mock/template/${itf.id}`} target="_blank" rel="noopener noreferrer"><GoLink className="fontsize-14" /></a>
: null}
</div>
<pre className='body'>{
JSON.stringify(template, (k, v) => {
if (typeof v === 'function') return v.toString()
if (v !== undefined && v !== null && v.exec) return v.toString()
else return v
<pre className="body">{
JSON.stringify(template, (_: any, v) => {
if (typeof v === 'function') { return v.toString() }
if (v !== undefined && v !== null && v.exec) { return v.toString() } else { return v }
}, 2)
}</pre>
</div>
<div className='result-mocked col-6'>
<div className='header'>
<span className='title'>{label}</span>
<div className="result-mocked col-6">
<div className="header">
<span className="title">{label}</span>
{scope === 'response'
? <a href={`${serve}/app/mock/data/${itf.id}`} target='_blank'><GoLink className='mr6 fontsize-14' /></a>
? <a href={`${serve}/app/mock/data/${itf.id}`} target="_blank" rel="noopener noreferrer"><GoLink className="mr6 fontsize-14" /></a>
: null}
<Link to='' onClick={e => this.remock(e)}><GoSync className='mr6 fontsize-14' onAnimationEnd={e => this.removeAnimateClass(e)} /></Link>
<Link to="" onClick={e => this.remock(e)}><GoSync className="mr6 fontsize-14" onAnimationEnd={e => this.removeAnimateClass(e)} /></Link>
</div>
<pre className='body'>{JSON.stringify(data, null, 2)}</pre>
<pre className="body">{JSON.stringify(data, null, 2)}</pre>
</div>
{scope === 'response'
? <div className='result-valid col-12'>
? <div className="result-valid col-12">
{!valid.length
? <span><GoBeer className='mr6 fontsize-20' /> </span>
: <span><GoBug className='mr6 fontsize-20' /></span>
? <span><GoBeaker className="mr6 fontsize-20" /> </span>
: <span><GoBug className="mr6 fontsize-20" /></span>
}
</div>
: null
@ -103,15 +103,15 @@ class Previewer extends Component {
}
return <div>...</div>
}
remock = (e) => {
remock = (e: any) => {
e.preventDefault()
let target = e.currentTarget.firstChild
const target = e.currentTarget.firstChild
target.classList.add('animated')
target.classList.add('rotateIn')
this.forceUpdate()
}
removeAnimateClass = (e) => {
let target = e.currentTarget
removeAnimateClass = (e: any) => {
const target = e.currentTarget
target.classList.remove('animated')
target.classList.remove('rotateIn')
}

@ -1,181 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, Link, replace, StoreStateRouterLocationURI } from '../../family'
import { DialogController } from '../utils'
import { serve } from '../../relatives/services/constant'
import InterfaceForm from './InterfaceForm'
import { getRelativeUrl } from '../../utils/URLUtils'
import './InterfaceSummary.css'
export const BODY_OPTION = {
FORM_DATA: 'FORM_DATA',
FORM_URLENCODED: 'FORM_URLENCODED',
RAW: 'RAW',
BINARY: 'BINARY'
}
export const REQUEST_PARAMS_TYPE = {
HEADERS: 'HEADERS',
QUERY_PARAMS: 'QUERY_PARAMS',
BODY_PARAMS: 'BODY_PARAMS'
}
export function rptFromStr2Num(rpt) {
let pos = 2
if (rpt === 'HEADERS') {
pos = 1
} else if (rpt === 'BODY_PARAMS') {
pos = 3
}
return pos
}
class InterfaceSummary extends Component {
constructor(props) {
super(props)
this.state = {
bodyOption: BODY_OPTION.FORM_DATA,
requestParamsType: props.itf.method === 'POST' ? REQUEST_PARAMS_TYPE.BODY_PARAMS : REQUEST_PARAMS_TYPE.QUERY_PARAMS
}
this.changeMethod = this.changeMethod.bind(this)
this.changeHandler = this.changeHandler.bind(this)
this.switchBodyOptions = this.switchBodyOption.bind(this)
this.switchRequestParamsType = this.switchRequestParamsType.bind(this)
this.state.requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS && props.stateChangeHandler(this.state)
}
static contextTypes = {
store: PropTypes.object.isRequired,
onDeleteInterface: PropTypes.func.isRequired
}
static propTypes = {
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
active: PropTypes.bool.isRequired,
editable: PropTypes.bool.isRequired,
stateChangeHandler: PropTypes.func.isRequired
}
componentDidUpdate() {
}
switchBodyOption(val) {
return () => {
this.setState({
bodyOption: val
}, () => {
this.props.stateChangeHandler(this.state)
})
}
}
switchRequestParamsType(val) {
return () => {
this.setState({
requestParamsType: val
}, () => {
this.props.stateChangeHandler(this.state)
})
}
}
changeMethod(method) {
this.setState({ method })
}
changeStatus(status) {
this.setState({ status })
}
changeHandler(e) {
this.setState({
[e.target.name]: e.target.value
})
}
render() {
const { repository = {}, mod = {}, itf = {}, editable } = this.props
const { requestParamsType } = this.state
if (!itf.id) return null
return (
<div className='InterfaceSummary'>
<div className='header'>
<span className='title'>
{itf.name}
</span>
{/* TODO 2.2 √模板接口、√数据接口、JSONSchema 接口 */}
{/* TODO 2.2 权限控制,被别人锁定时不能编辑和删除 */}
{/* TODO 2.2 这里的接口编辑和右侧的编辑容易引起歧义,很难受 */}
<span className='hide'>
<DialogController content={<InterfaceForm title='修改接口' repository={repository} mod={mod} itf={itf} />} onResolved={this.handleUpdate}>
<Link to='' onClick={e => e.preventDefault()} title='修改接口' className='edit'>编辑</Link>
</DialogController>
<Link to='' onClick={e => this.handleDelete(e, itf)} className='delete'>删除</Link>
</span>
</div>
<ul className='body'>
<li>
<span className='label'>地址</span>
<a href={`${serve}/app/mock/${repository.id}${getRelativeUrl(itf.url || '')}`} target='_blank'>{itf.url}</a>
</li>
<li><span className='label'>类型</span>
{itf.method}
</li>
<li><span className='label'>状态码</span>
{itf.status}
</li>
{itf.description &&
<li><span className='label'>简介</span>{itf.description}</li>
}
{editable &&
<ul className='nav nav-tabs' role='tablist'>
<li className='nav-item' onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.HEADERS)}>
<a className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.HEADERS ? 'active' : ''}`} role='tab' data-toggle='tab'>headers</a>
</li>
<li className='nav-item' onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.QUERY_PARAMS)}>
<a className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS ? 'active' : ''}`} role='tab' data-toggle='tab'>Query Params</a>
</li>
<li className='nav-item' onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.BODY_PARAMS)}>
<a className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? 'active' : ''}`} role='tab' data-toggle='tab'>Body Params</a>
</li>
</ul>
}
</ul>
{editable && requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS
? <div className='body-options'>
<div className='form-check form-check-inline' onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}>
<input className='form-check-input' type='radio' name='inlineRadioOptions' id='inlineRadio1' value='option1' />
<label className='form-check-label' htmlFor='inlineRadio1'>form-data</label>
</div>
<div className='form-check form-check-inline' onClick={this.switchBodyOption(BODY_OPTION.FORM_URLENCODED)}>
<input className='form-check-input' type='radio' name='inlineRadioOptions' id='inlineRadio2' value='option2' />
<label className='form-check-label' htmlFor='inlineRadio2'>x-www-form-urlencoded</label>
</div>
<div className='form-check form-check-inline' onClick={this.switchBodyOption(BODY_OPTION.RAW)}>
<input className='form-check-input' type='radio' name='inlineRadioOptions' id='inlineRadio3' value='option3' />
<label className='form-check-label' htmlFor='inlineRadio3'>raw</label>
</div>
<div className='form-check form-check-inline' onClick={this.switchBodyOption(BODY_OPTION.BINARY)}>
<input className='form-check-input' type='radio' name='inlineRadioOptions' id='inlineRadio4' value='option4' />
<label className='form-check-label' htmlFor='inlineRadio4'>binary</label>
</div>
</div> : null
}
</div>
)
}
handleDelete = (e, itf) => {
e.preventDefault()
let message = '接口被删除后不可恢复!\n确认继续删除吗'
if (window.confirm(message)) {
let { onDeleteInterface } = this.context
onDeleteInterface(itf.id, () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
let deleteHref = this.props.active ? uri.removeSearch('itf').href() : uri.href()
store.dispatch(replace(deleteHref))
})
}
}
handleUpdate = () => {
}
}
export default InterfaceSummary

@ -4,4 +4,11 @@
.form-control
max-width: 500px
.body-options
margin: 8px
margin: 8px
ul.summary
padding: 0
li
list-style: none
margin: 0
padding: 0
margin-bottom: 5px

@ -0,0 +1,285 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Link, replace, StoreStateRouterLocationURI, PropTypes } from '../../family'
import { DialogController } from '../utils'
import { serve } from '../../relatives/services/constant'
import InterfaceForm from './InterfaceForm'
import { GoDiffAdded } from 'react-icons/go'
import SpinInline from '../utils/SpinInline'
import { getRelativeUrl } from '../../utils/URLUtils'
import './InterfaceSummary.css'
import { RootState } from 'actions/types'
export const BODY_OPTION = {
FORM_DATA: 'FORM_DATA',
FORM_URLENCODED: 'FORM_URLENCODED',
RAW: 'RAW',
BINARY: 'BINARY',
}
export const REQUEST_PARAMS_TYPE = {
HEADERS: 'HEADERS',
QUERY_PARAMS: 'QUERY_PARAMS',
BODY_PARAMS: 'BODY_PARAMS',
}
export function rptFromStr2Num(rpt: any) {
let pos = 2
if (rpt === 'HEADERS') {
pos = 1
} else if (rpt === 'BODY_PARAMS') {
pos = 3
}
return pos
}
type InterfaceSummaryProps = {
store: object,
[x: string]: any,
}
type InterfaceSummaryState = {
bodyOption?: any,
requestParamsType?: any,
method?: any,
status?: any,
[x: string]: any,
}
class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummaryState> {
static contextTypes = {
onAddForeignRoomCase: PropTypes.func.isRequired,
onDeleteInterface: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = {
bodyOption: BODY_OPTION.FORM_DATA,
requestParamsType: props.itf.method === 'POST' ? REQUEST_PARAMS_TYPE.BODY_PARAMS : REQUEST_PARAMS_TYPE.QUERY_PARAMS,
}
this.changeMethod = this.changeMethod.bind(this)
this.changeHandler = this.changeHandler.bind(this)
this.switchBodyOption = this.switchBodyOption.bind(this)
this.switchRequestParamsType = this.switchRequestParamsType.bind(this)
this.state.requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS && props.stateChangeHandler(this.state)
}
switchBodyOption(val: any) {
return () => {
this.setState(
{
bodyOption: val,
},
() => {
this.props.stateChangeHandler(this.state)
}
)
}
}
switchRequestParamsType(val: any) {
return () => {
this.setState(
{
requestParamsType: val,
},
() => {
this.props.stateChangeHandler(this.state)
}
)
}
}
renderRoom() {
const { itf = {}, room = {}, repository = {} } = this.props
if (!room || !room[repository.id + '_' + itf.id] || !room[repository.id] || room[repository.id] === 'pending') {
return
}
if (room[repository.id + '_' + itf.id] === 'pending') {
return (
<li>
<span className="label">Room</span>
<SpinInline />
</li>
)
}
const roomData = room[repository.id + '_' + itf.id]
const requestStatus = room['+' + repository.id + '_' + itf.id]
return (
<li>
<span className="label">Room</span>
{roomData.coverage ? (
<Link target="_blank" to={`http://room.daily.taobao.net/index.html#/design?projectId=${roomData.roomProjectId}`}>
</Link>
) : (
!requestStatus && <span></span>
)}
{!requestStatus && (
<span>
<Link to="" className="ml10" onClick={e => this.createCase(e, repository.id, itf.id, '普通')}>
<GoDiffAdded />
</Link>
<Link to="" className="ml10" onClick={e => this.createCase(e, repository.id, itf.id, '边界')}>
<GoDiffAdded />
</Link>
</span>
)}
{requestStatus === 'pending' && (
<span>
<span className="ml10">
<GoDiffAdded /> {this.state.name}{' '}
</span>
<SpinInline />
</span>
)}
{requestStatus === 'failed' && (
<span className="ml10">
<GoDiffAdded />
{this.state.name}
<Link to="" className="ml10" onClick={e => this.createCase(e, repository.id, itf.id, this.state.name)}>
{' '}
</Link>
</span>
)}
{requestStatus === 'success' && (
<span className="ml10">
<GoDiffAdded /> {this.state.name}
<Link target="_blank" to={`http://room.daily.taobao.net/index.html#/design?projectId=${roomData.roomProjectId}`}>
</Link>
</span>
)}
</li>
)
}
changeMethod(method: any) {
this.setState({ method })
}
changeStatus(status: any) {
this.setState({ status })
}
changeHandler(e: any) {
this.setState({
[e.target.name]: e.target.value,
})
}
render() {
const { repository = {}, mod = {}, itf = {}, editable } = this.props
const { requestParamsType } = this.state
if (!itf.id) { return null }
return (
<div className="InterfaceSummary">
<div className="header">
<span className="title">{itf.name}</span>
<span className="hide">
<DialogController content={<InterfaceForm title="修改接口" repository={repository} mod={mod} itf={itf} />} onResolved={this.handleUpdate}>
<Link to="" onClick={e => e.preventDefault()} title="修改接口" className="edit">
</Link>
</DialogController>
<Link to="" onClick={e => this.handleDelete(e, itf)} className="delete">
</Link>
</span>
</div>
<ul className="summary">
<li>
<span className="label"></span>
<a href={`${serve}/app/mock/${repository.id}${getRelativeUrl(itf.url || '')}`} target="_blank" rel="noopener noreferrer">
{itf.url}
</a>
</li>
<li>
<span className="label"></span>
{itf.method}
</li>
<li>
<span className="label"></span>
{itf.status}
</li>
{itf.description && (
<li>
<span className="label"></span>
{itf.description}
</li>
)}
{this.renderRoom()}
{editable && (
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.HEADERS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.HEADERS ? 'active' : ''}`} role="tab" data-toggle="tab">
headers
</button>
</li>
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.QUERY_PARAMS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS ? 'active' : ''}`} role="tab" data-toggle="tab">
Query Params
</button>
</li>
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.BODY_PARAMS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? 'active' : ''}`} role="tab" data-toggle="tab">
Body Params
</button>
</li>
</ul>
)}
</ul>
{editable && requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? (
<div className="body-options">
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio1" value="option1" />
<label className="form-check-label" htmlFor="inlineRadio1">
form-data
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.FORM_URLENCODED)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio2" value="option2" />
<label className="form-check-label" htmlFor="inlineRadio2">
x-www-form-urlencoded
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.RAW)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio3" value="option3" />
<label className="form-check-label" htmlFor="inlineRadio3">
raw
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.BINARY)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio4" value="option4" />
<label className="form-check-label" htmlFor="inlineRadio4">
binary
</label>
</div>
</div>
) : null}
</div>
)
}
handleDelete = (e: any, itf: any) => {
e.preventDefault()
const message = '接口被删除后不可恢复!\n确认继续删除吗'
if (window.confirm(message)) {
const { onDeleteInterface } = this.context
onDeleteInterface(itf.id, () => {
const { router, replace } = this.props
const uri = StoreStateRouterLocationURI(router)
const deleteHref = this.props.active ? uri.removeSearch('itf').href() : uri.href()
replace(deleteHref)
})
}
}
handleUpdate = () => { /** empty */ }
createCase = (e: any, repositoryId: any, interfaceId: any, name: any) => {
e.preventDefault()
this.setState({ name })
const { onAddForeignRoomCase } = this.context
onAddForeignRoomCase({
id: repositoryId,
itf: interfaceId,
name,
})
// itf.repositoryId
}
}
const mapStateToProps = (state: RootState) => ({
room: state.foreign,
})
const mapDispatchToProps = {
replace,
}
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceSummary)

@ -1,95 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, Mock } from '../../family'
import { SmartTextarea } from '../utils'
//
const mockModule = process.env.NODE_ENV === 'development'
? () => Mock.mock({
name: '模块@CTITLE(4)',
description: '@CPARAGRAPH',
repositoryId: undefined
})
: () => ({
name: '',
description: '',
repositoryId: undefined
})
//
class ModuleForm extends Component {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddModule: PropTypes.func.isRequired,
onUpdateModule: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object
}
constructor (props) {
super(props)
let { mod } = this.props
this.state = mod ? { ...mod } : mockModule()
}
render () {
const { rmodal } = this.context
return (
<section>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<form className='form-horizontal w600' onSubmit={this.handleSubmit}>
<div className='rmodal-body'>
<div className='form-group row'>
<label className='col-sm-2 control-label'>名称</label>
<div className='col-sm-10'>
<input name='name' tabIndex={1} value={this.state.name} onChange={e => this.setState({ name: e.target.value })} className='form-control' placeholder='Name' spellCheck='false' autoFocus='true' required />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>简介</label>
<div className='col-sm-10'>
<SmartTextarea tabIndex={2} name='description' value={this.state.description} onChange={e => this.setState({ description: e.target.value })} className='form-control' placeholder='Description' spellCheck='false' rows='5' />
</div>
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</div>
</form>
</section>
)
}
handleSubmit = (e) => {
e.preventDefault()
let { onAddModule, onUpdateModule } = this.context
let { auth, repository } = this.props
let onAddOrUpdateModule = this.state.id ? onUpdateModule : onAddModule
let mod = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id
})
let { rmodal } = this.context
rmodal.close()
onAddOrUpdateModule(mod, () => {
if (rmodal) rmodal.resolve()
})
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleForm)

@ -0,0 +1,114 @@
import React, { Component } from 'react'
import { PropTypes, connect, Mock } from '../../family'
import { SmartTextarea } from '../utils'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
// 模拟数据
const mockModule = process.env.NODE_ENV === 'development'
? () => Mock.mock({
name: '模块@CTITLE(4)',
description: '@CPARAGRAPH',
repositoryId: undefined,
})
: () => ({
name: '',
description: '',
repositoryId: undefined,
})
// 展示组件
class ModuleForm extends Component<any, any> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddModule: PropTypes.func.isRequired,
onUpdateModule: PropTypes.func.isRequired,
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object,
}
constructor(props: any) {
super(props)
const { mod } = this.props
this.state = mod ? { ...mod } : mockModule()
}
render() {
const { rmodal } = this.context
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={1}
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoFocus={true}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<SmartTextarea
tabIndex={2}
name="description"
value={this.state.description}
onChange={(e: any) => this.setState({ description: e.target.value })}
className="form-control"
placeholder="Description"
spellCheck={false}
rows="5"
/>
</div>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" style={{ marginRight: 8 }} variant="contained" color="primary"></Button>
<Button onClick={() => rmodal.close()} ></Button>
</div>
</div>
</div>
</form>
</section>
)
}
handleSubmit = (e: any) => {
e.preventDefault()
const { onAddModule, onUpdateModule } = this.context
const { auth, repository } = this.props
const onAddOrUpdateModule = this.state.id ? onUpdateModule : onAddModule
const mod = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
})
const { rmodal } = this.context
rmodal.close()
onAddOrUpdateModule(mod, () => {
if (rmodal) { rmodal.resolve() }
})
}
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleForm)

@ -1,126 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, replace, URI, StoreStateRouterLocationURI } from '../../family'
import { RModal, RSortable } from '../utils'
import ModuleForm from './ModuleForm'
import { GoPencil, GoTrashcan, GoPackage } from 'react-icons/lib/go'
class Module extends Component {
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired
}
static contextTypes = {
store: PropTypes.object,
onDeleteModule: PropTypes.func
}
constructor (props) {
super(props)
this.state = { update: false }
}
render () {
let { store } = this.context
let { auth, repository, mod } = this.props
let uri = StoreStateRouterLocationURI(store).removeSearch('itf')
let selectHref = URI(uri).setSearch('mod', mod.id).href()
return (
<div className='Module clearfix'>
<Link to={selectHref} className='name'>{mod.name}</Link>
<div className='toolbar'>
{/* 编辑权限:拥有者或者成员 */}
{repository.owner.id === auth.id || repository.members.find(itme => itme.id === auth.id)
? <span className='fake-link' onClick={e => this.setState({ update: true })}><GoPencil /></span>
: null
}
{repository.owner.id === auth.id || repository.members.find(itme => itme.id === auth.id)
? <span className='fake-link' onClick={e => this.handleDelete(e, mod)}><GoTrashcan /></span>
: null
}
</div>
<RModal when={this.state.update} onClose={e => this.setState({ update: false })} onResolve={this.handleUpdate}>
<ModuleForm title='修改模块' mod={mod} repository={repository} />
</RModal>
</div>
)
}
handleUpdate = (e) => {
let { store } = this.context
store.dispatch(replace(StoreStateRouterLocationURI(store).href()))
}
handleDelete = (e, mod) => {
e.preventDefault()
let message = `模块被删除后不可恢复,并且会删除相关的接口!\n确认继续删除『#${mod.id} ${mod.name}』吗?`
if (window.confirm(message)) {
this.context.onDeleteModule(this.props.mod.id, () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
let deleteHref = this.props.active ? URI(uri).removeSearch('mod').href() : uri.href()
store.dispatch(replace(deleteHref))
}, this.props.repository.id)
}
}
}
class ModuleList extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
onSortModuleList: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mods: PropTypes.array,
mod: PropTypes.object
}
static childContextTypes = {}
getChildContext () {}
constructor (props) {
super(props)
this.state = { create: false }
}
render () {
let { auth, repository = {}, mods = [], mod = {} } = this.props
let isOwned = repository.owner.id === auth.id
let isJoined = repository.members.find(itme => itme.id === auth.id)
return (
<RSortable onChange={this.handleSort} disabled={!isOwned && !isJoined}>
<ul className='ModuleList clearfix'>
{mods.map((item, index) =>
<li key={item.id} className={item.id === mod.id ? 'active sortable' : 'sortable'} data-id={item.id}>
<Module key={item.id} mod={item} active={item.id === mod.id} repository={repository} auth={auth} />
</li>
)}
{/* 编辑权限:拥有者或者成员 */}
{isOwned || isJoined
? <li>
<span className='fake-link' onClick={e => this.setState({ create: true })}>
<GoPackage className='fontsize-14' /> 新建模块
</span>
<RModal when={this.state.create} onClose={e => this.setState({ create: false })} onResolve={this.handleCreate}>
<ModuleForm title='新建模块' repository={repository} />
</RModal>
</li>
: null
}
</ul>
</RSortable>
)
}
handleCreate = () => {
let { store } = this.context
store.dispatch(replace(StoreStateRouterLocationURI(store).href()))
}
handleSort = (e, sortable) => {
let { onSortModuleList } = this.context
onSortModuleList(sortable.toArray())
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleList)

@ -0,0 +1,112 @@
import React, { Component } from 'react'
import { connect, Link, replace, StoreStateRouterLocationURI } from '../../family'
import { RModal, RSortable } from '../utils'
import ModuleForm from './ModuleForm'
import { GoPencil, GoTrashcan, GoPackage } from 'react-icons/go'
import { RootState } from 'actions/types'
class ModuleBase extends Component<any, any> {
constructor(props: any) {
super(props)
this.state = { update: false }
}
render() {
const { auth, repository, mod, router } = this.props
const uri = StoreStateRouterLocationURI(router).removeSearch('itf')
const selectHref = uri.setSearch('mod', mod.id).href()
return (
<div className="Module clearfix">
<Link to={selectHref} className="name">{mod.name}</Link>
<div className="toolbar">
{/* 编辑权限:拥有者或者成员 */}
{repository.owner.id === auth.id || repository.members.find((item: any) => item.id === auth.id)
? <span className="fake-link" onClick={() => this.setState({ update: true })}><GoPencil /></span>
: null
}
{repository.owner.id === auth.id || repository.members.find((item: any) => item.id === auth.id)
? <span className="fake-link" onClick={e => this.handleDelete(e, mod)}><GoTrashcan /></span>
: null
}
</div>
<RModal when={this.state.update} onClose={() => this.setState({ update: false })} onResolve={this.handleUpdate}>
<ModuleForm title="修改模块" mod={mod} repository={repository} />
</RModal>
</div>
)
}
handleUpdate = () => {
this.props.replace(StoreStateRouterLocationURI(this.props.router).href())
}
handleDelete = (e: any, mod: any) => {
const { router } = this.props
e.preventDefault()
const message = `模块被删除后不可恢复,并且会删除相关的接口!\n确认继续删除『#${mod.id} ${mod.name}』吗?`
if (window.confirm(message)) {
this.context.onDeleteModule(this.props.mod.id, () => {
const { store } = this.context
const uri = StoreStateRouterLocationURI(router)
const deleteHref = this.props.active ? uri.removeSearch('mod').href() : uri.href()
store.dispatch(replace(deleteHref))
}, this.props.repository.id)
}
}
}
const Module = connect((state: any) => ({
router: state.router,
}))(ModuleBase)
class ModuleList extends Component<any, any> {
constructor(props: any) {
super(props)
this.state = { create: false }
}
render() {
const { auth, repository = {}, mods = [], mod = {} } = this.props
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((item: any) => item.id === auth.id)
return (
<RSortable onChange={this.handleSort} disabled={!isOwned && !isJoined}>
<ul className="ModuleList clearfix">
{mods.map((item: any) =>
<li key={item.id} className={item.id === mod.id ? 'active sortable' : 'sortable'} data-id={item.id}>
<Module key={item.id} mod={item} active={item.id === mod.id} repository={repository} auth={auth} />
</li>
)}
{/* 编辑权限:拥有者或者成员 */}
{isOwned || isJoined
? <li>
<span className="fake-link" onClick={() => this.setState({ create: true })}>
<GoPackage className="fontsize-14" />
</span>
<RModal when={this.state.create} onClose={() => this.setState({ create: false })} onResolve={this.handleCreate}>
<ModuleForm title="新建模块" repository={repository} />
</RModal>
</li>
: null
}
</ul>
</RSortable>
)
}
handleCreate = () => {
const { router, replace } = this.props
replace(StoreStateRouterLocationURI(router).href())
}
handleSort = (_: any, sortable: any) => {
const { onSortModuleList } = this.context
onSortModuleList(sortable.toArray())
}
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
router: state.router,
})
const mapDispatchToProps = ({
replace,
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleList)

@ -1,106 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link } from '../../family'
import { moveInterface } from '../../actions/interface'
const OP_MOVE = 1
const OP_COPY = 2
class MoveInterfaceForm extends Component {
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
onAddInterface: PropTypes.func.isRequired,
onUpdateInterface: PropTypes.func.isRequired
}
static propTypes = {
title: PropTypes.string.isRequired,
repository: PropTypes.object.isRequired,
itfId: PropTypes.number.isRequired,
moveInterface: PropTypes.func.isRequired
}
constructor (props) {
super(props)
const { repository } = props
let modId = 0
if (repository.modules.length > 0) {
modId = repository.modules[0].id
}
this.state = {
op: OP_MOVE, // 1 move, 2 copy
modId
}
}
render () {
const { rmodal } = this.context
const { repository } = this.props
const { modId, op } = this.state
return (
<section>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<form className='form-horizontal w600' onSubmit={this.handleSubmit} >
<div className='rmodal-body'>
<div className='form-group row'>
<label className='col-sm-2 control-label'>模块</label>
<div className='col-sm-10'>
<select className='form-control' onChange={e => { this.setState({ modId: +e.target.value }) }}>
{repository.modules.map(x => <option key={x.id} value={x.id} checked={x.id === modId}>{x.name}</option>)}
</select>
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>选项</label>
<div className='col-sm-10'>
<div className='col-sm-10'>
<div className='form-check'>
<input className='form-check-input' type='radio' name='op' id='gridRadios1' value='1' checked={op === OP_MOVE} onChange={() => { this.setState({ op: OP_MOVE }) }} />
<label className='form-check-label' htmlFor='gridRadios1'> 移动 </label>
</div>
<div className='form-check'>
<input className='form-check-input' type='radio' name='op' id='gridRadios2' value='2' checked={op === OP_COPY} onChange={() => { this.setState({ op: OP_COPY }) }} />
<label className='form-check-label' htmlFor='gridRadios2'> 复制 </label>
</div>
</div>
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate () {
this.context.rmodal.reposition()
}
handleSubmit = (e) => {
e.preventDefault()
const params = {
modId: this.state.modId,
op: this.state.op,
itfId: this.props.itfId,
repoId: this.props.repository.id
}
this.props.moveInterface(params, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
const mapStateToProps = (state) => ({
})
const mapDispatchToProps = ({
moveInterface
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(MoveInterfaceForm)

@ -0,0 +1,115 @@
import React, { useState, useContext, useEffect } from 'react'
import { moveInterface } from '../../actions/interface'
import { Button, Select, MenuItem, FormControl, RadioGroup, FormControlLabel, Radio, Theme, makeStyles } from '@material-ui/core'
import { useDispatch } from 'react-redux'
import { ModalContext } from 'components/utils/RModal'
export const OP_MOVE = 1
export const OP_COPY = 2
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
minHeight: 300,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
}))
interface Props {
title: string
repository: any
itfId: number
}
// constructor(props: any) {
// super(props)
// const { repository } = props
// this.state = { modId, op: OP_MOVE }
// }
export default function MoveInterfaceForm(props: Props) {
const { repository, title, itfId } = props
const classes = useStyles()
let modIdInit = 0
if (repository.modules.length > 0) {
modIdInit = repository.modules[0].id
}
const [modId, setModId] = useState(modIdInit)
const [op, setOp] = useState(OP_MOVE)
const dispatch = useDispatch()
const rmodal = useContext(ModalContext)
useEffect(() => {
rmodal && rmodal.reposition()
}, [rmodal])
const handleSubmit = (e: any) => {
e.preventDefault()
const params = {
modId,
op,
itfId,
}
dispatch(moveInterface(params, () => {
rmodal && rmodal.resolve()
}))
}
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{title}</span>
</div>
<form className="form-horizontal w600" onSubmit={handleSubmit} >
<div className="rmodal-body">
<div className={classes.formTitle}></div>
<FormControl>
<Select onChange={e => setModId(+(e.target.value as any as string))} value={modId} fullWidth={true}>
{repository.modules.map((x: any) => <MenuItem key={x.id} value={x.id} >{x.name}</MenuItem>)}
</Select>
</FormControl>
<div className={classes.formTitle}></div>
<RadioGroup
name="radioListOp"
value={String(op)}
onChange={e => {setOp(+(e.target as any).value) }}
row={true}
>
<FormControlLabel value={String(OP_COPY)} control={<Radio />} label="复制" />
<FormControlLabel value={String(OP_MOVE)} control={<Radio />} label="移动" />
</RadioGroup>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" variant="contained" color="primary" style={{ marginRight: 8 }}></Button>
<Button onClick={() => rmodal && rmodal.close()}></Button>
</div>
</div>
</div>
</div>
</form>
</section >
)
}

@ -1,137 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import SmartTextarea from '../utils/SmartTextarea'
export const TYPES = ['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp']
//
const mockProperty = process.env.NODE_ENV === 'development'
? () => Mock.mock({
'scope|1': ['request', 'response'],
name: '@WORD(6)',
'type|1': TYPES,
'value|1': ['@INT', '@FLOAT', '@TITLE', '@NAME'],
description: '@CSENTENCE',
parentId: -1,
interfaceId: '@NATURAL',
moduleId: '@NATURAL',
repositoryId: '@NATURAL'
})
: () => ({
scope: 'response',
name: '',
type: 'String',
value: '',
description: '',
parentId: -1,
interfaceId: undefined,
moduleId: undefined,
repositoryId: undefined
})
class PropertyForm extends Component {
static propTypes = {
scope: PropTypes.string.isRequired,
parent: PropTypes.object,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired
}
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
handleAddMemoryProperty: PropTypes.func.isRequired
}
constructor (props) {
super(props)
this.state = mockProperty()
}
render () {
const { rmodal } = this.context
return (
<section>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<form className='form-horizontal w600' onSubmit={this.handleSubmit} >
<div className='rmodal-body'>
<div className='form-group row' style={{}}>
<label className='col-sm-2 control-label'>名称</label>
<div className='col-sm-10'>
<input name='name' tabIndex={1} value={this.state.name} onChange={e => this.setState({ name: e.target.value })} className='form-control' placeholder='Name' spellCheck='false' autoFocus='true' required />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>类型</label>
<div className='col-sm-10'>
<select name='type' tabIndex={2} value={this.state.type} onChange={e => this.setState({ type: e.target.value })} className='form-control'>
{TYPES.map(type =>
<option key={type} value={type}>{type}</option>
)}
</select>
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>生成规则</label>
<div className='col-sm-10'>
<input name='rule' tabIndex={3} value={this.state.rule} onChange={e => this.setState({ rule: e.target.value })} className='form-control' placeholder='Rule' spellCheck='false' />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>初始值</label>
<div className='col-sm-10'>
<input name='value' tabIndex={4} value={this.state.value} onChange={e => this.setState({ value: e.target.value })} className='form-control' placeholder='Value' spellCheck='false' />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>简介</label>
<div className='col-sm-10'>
<SmartTextarea tabIndex={5} name='description' value={this.state.description} onChange={e => this.setState({ description: e.target.value })} className='form-control' placeholder='Description' spellCheck='false' rows='5' />
</div>
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate () {
this.context.rmodal.reposition()
}
handleSubmit = (e) => {
e.preventDefault()
let { auth, repository, mod, itf, scope, parent = { id: -1 } } = this.props
let { handleAddMemoryProperty } = this.context
let property = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
scope,
parentId: parent.id
})
handleAddMemoryProperty(property, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(PropertyForm)

@ -0,0 +1,178 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Mock from 'mockjs'
import SmartTextarea from '../utils/SmartTextarea'
import { Button } from '@material-ui/core'
export const TYPES = ['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp']
// 模拟数据
const mockProperty = process.env.NODE_ENV === 'development'
? () => Mock.mock({
'scope|1': ['request', 'response'],
name: '@WORD(6)',
'type|1': TYPES,
'value|1': ['@INT', '@FLOAT', '@TITLE', '@NAME'],
description: '@CSENTENCE',
parentId: -1,
interfaceId: '@NATURAL',
moduleId: '@NATURAL',
repositoryId: '@NATURAL',
})
: () => ({
scope: 'response',
name: '',
type: 'String',
value: '',
description: '',
parentId: -1,
interfaceId: undefined,
moduleId: undefined,
repositoryId: undefined,
})
class PropertyForm extends Component<any, any> {
static propTypes = {
scope: PropTypes.string.isRequired,
parent: PropTypes.object,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
}
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
handleAddMemoryProperty: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = mockProperty()
}
render() {
const { rmodal } = this.context
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit} >
<div className="rmodal-body">
<div className="form-group row" style={{}}>
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={1}
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoFocus={true}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<select
name="type"
tabIndex={2}
value={this.state.type}
onChange={e => this.setState({ type: e.target.value })}
className="form-control"
>
{TYPES.map(type =>
<option key={type} value={type}>{type}</option>
)}
</select>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="rule"
tabIndex={3}
value={this.state.rule}
onChange={e => this.setState({ rule: e.target.value })}
className="form-control"
placeholder="Rule"
spellCheck={false}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="value"
tabIndex={4}
value={this.state.value}
onChange={e => this.setState({ value: e.target.value })}
className="form-control"
placeholder="Value"
spellCheck={false}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<SmartTextarea
tabIndex={5}
name="description"
value={this.state.description}
onChange={(e: any) => this.setState({ description: e.target.value })}
className="form-control"
placeholder="Description"
spellCheck={false}
rows="5"
/>
</div>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" variant="contained" color="primary" style={{ marginRight: 8 }}></Button>
<Button onClick={() => rmodal.close()}></Button>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
handleSubmit = (e: any) => {
e.preventDefault()
const { auth, repository, mod, itf, scope, parent = { id: -1 } } = this.props
const { handleAddMemoryProperty } = this.context
const property = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
scope,
parentId: parent.id,
})
handleAddMemoryProperty(property, () => {
const { rmodal } = this.context
if (rmodal) { rmodal.resolve() }
})
}
}
const mapStateToProps = (state: any) => ({
auth: state.auth,
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(PropertyForm)

@ -1,267 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link } from '../../family'
import { Tree, SmartTextarea, RModal, RSortable } from '../utils'
import PropertyForm from './PropertyForm'
import Importer from './Importer'
import Previewer from './InterfacePreviewer'
import { GoMention, GoFileCode, GoEye, GoPlus, GoTrashcan, GoQuestion } from 'react-icons/lib/go'
import { rptFromStr2Num } from './InterfaceSummary'
import './PropertyList.css'
export const RequestPropertyListPreviewer = (props) => (
<Previewer {...props} />
)
export const ResponsePropertyListPreviewer = (props) => (
<Previewer {...props} />
)
// DONE 2.2
// DONE 2.2 URL URL URL
// DONE 2.2
// DONE 2.2
// DONE 2.2
// TODO 2.3
class SortableTreeTableHeader extends Component {
render() {
let { editable } = this.props
return (
<div className='SortableTreeTableHeader'>
<div className='flex-row'>
{/* DONE 2.1 每列增加帮助 Tip */}
{editable && <div className='th operations' />}
<div className='th name'>名称</div>
<div className='th type'>必选</div>
<div className='th type'>类型</div>
{/* TODO 2.3 规则编辑器 */}
<div className='th rule'>
生成规则
<a href='https://github.com/nuysoft/Mock/wiki/Syntax-Specification' rel="noopener noreferrer" className='helper' target='_blank'><GoQuestion /></a>
</div>
<div className='th value'>初始值</div>{/* 对象和数组也允许设置初始值 */}
<div className='th desc'>简介</div>
</div>
</div>
)
}
}
const PropertyLabel = (props) => {
const { pos } = props
if (pos === 1) {
return <label className='ml5 badge badge-danger'>HEAD</label>
} else if (pos === 3) {
return <label className='ml5 badge badge-primary'>BODY</label>
} else {
return <label className='ml5 badge badge-secondary'>QUERY</label>
}
}
class SortableTreeTableRow extends Component {
render() {
let { property, editable } = this.props
let { handleClickCreateChildPropertyButton, handleDeleteMemoryProperty, handleChangePropertyField, handleSortProperties } = this.props
return (
<RSortable group={property.depth} handle='.SortableTreeTableRow' disabled={!editable} onChange={handleSortProperties}>
<div className={`RSortableWrapper depth${property.depth}`}>
{property.children.sort((a, b) => a.priority - b.priority).map(item =>
<div key={item.id} className='SortableTreeTableRow' data-id={item.id}>
<div className='flex-row'>
{editable &&
<div className='td operations nowrap'>
{(item.type === 'Object' || item.type === 'Array')
? <Link to='' onClick={e => { e.preventDefault(); handleClickCreateChildPropertyButton(item) }}><GoPlus className='fontsize-14 color-6' /></Link>
: null}
<Link to='' onClick={e => handleDeleteMemoryProperty(e, item)}><GoTrashcan className='fontsize-14 color-6' /></Link>
</div>
}
<div className={`td payload name depth-${item.depth} nowrap`}>
{!editable
? <span className='nowrap'>{item.name}{item.scope === 'request' && item.depth === 0 ? <PropertyLabel pos={item.pos} /> : null}</span>
: <input value={item.name} onChange={e => handleChangePropertyField(item.id, 'name', e.target.value)} className='form-control editable' spellCheck='false' placeholder='' />
}
</div>
<div className={`td payload type depth-${item.depth} nowrap`}>
{!editable
? <span className='nowrap'>{item.required ? '✔️' : ''}</span>
: <input type='checkbox' checked={!!item.required} onChange={e => handleChangePropertyField(item.id, 'required', e.target.checked)} />
}
</div>
<div className='td payload type'>
{!editable
? <span className='nowrap'>{item.type}</span>
: <select value={item.type} onChange={e => handleChangePropertyField(item.id, 'type', e.target.value)} className='form-control editable'>
{['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp'].map(type =>
<option key={type} value={type}>{type}</option>
)}
</select>
}
</div>
<div className='td payload rule nowrap'>
{!editable
? <span className='nowrap'>{item.rule}</span>
: <input value={item.rule || ''} onChange={e => handleChangePropertyField(item.id, 'rule', e.target.value)} className='form-control editable' spellCheck='false' placeholder='' />
}
</div>
<div className='td payload value'>
{!editable
? <span>{item.value}</span>
: <SmartTextarea value={item.value || ''} onChange={e => handleChangePropertyField(item.id, 'value', e.target.value)} rows='1' className='form-control editable' spellCheck='false' placeholder='' />
}
</div>
<div className='td payload desc'>
{!editable
? <span>{item.description}</span>
: <SmartTextarea value={item.description || ''} onChange={e => handleChangePropertyField(item.id, 'description', e.target.value)} rows='1' className='form-control editable' spellCheck='false' placeholder='' />
}
</div>
</div>
{item.children && item.children.length ? <SortableTreeTableRow {...this.props} property={item} /> : null}
</div>
)}
</div>
</RSortable>
)
}
}
class SortableTreeTable extends Component {
render() {
let { root, editable } = this.props
return (
<div className={`SortableTreeTable ${editable ? 'editable' : ''}`}>
<SortableTreeTableHeader {...this.props} />
<SortableTreeTableRow {...this.props} property={root} />
</div>
)
}
}
class PropertyList extends Component {
static contextTypes = {
store: PropTypes.object.isRequired,
handleDeleteMemoryProperty: PropTypes.func.isRequired,
handleChangeProperty: PropTypes.func.isRequired,
onDeleteProperty: PropTypes.func.isRequired,
onSortPropertyList: PropTypes.func.isRequired
}
static propTypes = {
title: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
scope: PropTypes.string.isRequired,
properties: PropTypes.array,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
/** optional */
bodyOption: PropTypes.string,
requestParamsType: PropTypes.string
}
constructor(props) {
super(props)
this.state = {
createProperty: false,
createChildProperty: false,
previewer: props.scope === 'response',
importer: false
}
}
render() {
let { title, label, scope, properties = [], repository = {}, mod = {}, itf = {} } = this.props
if (!itf.id) return null
let { editable, requestParamsType } = this.props // itf.locker && (itf.locker.id === auth.id)
const pos = rptFromStr2Num(requestParamsType)
let scopedProperties = properties.map(property => ({ ...property })).filter(property => property.scope === scope)
if (scope === 'request' && editable) {
scopedProperties = scopedProperties.filter(s => s.pos === pos)
}
return (
<section className='PropertyList'>
<div className='header clearfix'>
<span className='title'>{title || `${label}属性`}</span>
{/* DONE 2.2 新建按钮暂时合并到按扭组中,单独放出来有点混乱 */}
<div className='toolbar'>
<div className='btn-group'>
{editable && (
<button type='button' className='btn btn-secondary' onClick={this.handleClickCreatePropertyButton}>
<GoMention /> 新建
</button>
)}
{editable && (
<button type='button' className='btn btn-secondary' onClick={this.handleClickImporterButton}>
<GoFileCode className='fontsize-14 color-6' /> 导入
</button>
)}
<button type='button' className={`btn btn-secondary ${this.state.previewer && 'btn-success'}`} onClick={this.handleClickPreviewerButton}>
<GoEye className='fontsize-14' /> 预览
</button>
</div>
</div>
</div>
<div className='body'>
<SortableTreeTable root={Tree.arrayToTree(scopedProperties)} editable={editable}
handleClickCreateChildPropertyButton={this.handleClickCreateChildPropertyButton}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
handleChangePropertyField={this.handleChangePropertyField}
handleSortProperties={this.handleSortProperties} />
</div>
<div className='footer'>
{this.state.previewer && <Previewer scope={scope} label={label} properties={properties} itf={itf} />}
</div>
<RModal when={this.state.createProperty} onClose={e => this.setState({ createProperty: false })} onResolve={this.handleCreatePropertySucceeded}>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} />
</RModal>
<RModal when={!!this.state.createChildProperty} onClose={e => this.setState({ createChildProperty: false })} onResolve={this.handleCreatePropertySucceeded}>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} parent={this.state.createChildProperty} />
</RModal>
<RModal when={this.state.importer} onClose={e => this.setState({ importer: false })} onResolve={this.handleCreatePropertySucceeded}>
<Importer title={`导入${label}属性`} repository={repository} mod={mod} itf={itf} scope={scope} />
</RModal>
</section>
)
}
handleClickCreatePropertyButton = () => {
this.setState({ createProperty: true })
}
handleClickCreateChildPropertyButton = (item) => {
this.setState({ createChildProperty: item })
}
handleClickImporterButton = () => {
this.setState({ importer: true })
}
handleClickPreviewerButton = () => {
this.setState({ previewer: !this.state.previewer })
}
handleChangePropertyField = (id, key, value) => {
let { handleChangeProperty } = this.context
let { properties } = this.props
let property = properties.find(property => property.id === id)
handleChangeProperty({ ...property, [key]: value })
}
handleCreatePropertySucceeded = () => {
}
handleDeleteMemoryProperty = (e, property) => {
e.preventDefault()
let { handleDeleteMemoryProperty } = this.context
handleDeleteMemoryProperty(property)
}
handleSortProperties = (e, sortable) => {
let { properties } = this.props
let ids = sortable.toArray()
ids.forEach((id, index) => {
let property = properties.find(item => item.id === id || item.id === +id)
property.priority = index + 1
})
}
}
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(PropertyList)

@ -0,0 +1,311 @@
import React, { Component } from 'react'
import { PropTypes, Link } from '../../family'
import { Tree, SmartTextarea, RModal, RSortable } from '../utils'
import PropertyForm from './PropertyForm'
import Importer from './Importer'
import Previewer from './InterfacePreviewer'
import { GoPlus, GoTrashcan, GoQuestion } from 'react-icons/go'
import { rptFromStr2Num } from './InterfaceSummary'
import './PropertyList.css'
import { ButtonGroup, Button } from '@material-ui/core'
export const RequestPropertyListPreviewer = (props: any) => (
<Previewer {...props} />
)
export const ResponsePropertyListPreviewer = (props: any) => (
<Previewer {...props} />
)
// DONE 2.2 请求属性有什么用?有必要吗?有,用于订制响应数据。
// DONE 2.2 如何过滤模拟 URL 中额外的请求属性?解析 URL 中的参数到请求属性列表吗?可以在响应数据中引用 配置的请求参数 和 URL 中的额外参数。
// DONE 2.2 支持对属性排序
// DONE 2.2 支持对模块排序
// DONE 2.2 支持对接口排序
// TODO 2.3 检测重复属性
class SortableTreeTableHeader extends Component<any, any> {
render() {
const { editable } = this.props
return (
<div className="SortableTreeTableHeader">
<div className="flex-row">
{/* DONE 2.1 每列增加帮助 Tip */}
{editable && <div className="th operations" />}
<div className="th name"></div>
<div className="th type"></div>
<div className="th type"></div>
{/* TODO 2.3 规则编辑器 */}
<div className="th rule">
<a
href="https://github.com/nuysoft/Mock/wiki/Syntax-Specification"
rel="noopener noreferrer"
className="helper"
target="_blank"
>
<GoQuestion />
</a>
</div>
<div className="th value"></div>{/* 对象和数组也允许设置初始值 */}
<div className="th desc"></div>
</div>
</div>
)
}
}
const PropertyLabel = (props: any) => {
const { pos } = props
if (pos === 1) {
return <label className="ml5 badge badge-danger">HEAD</label>
} else if (pos === 3) {
return <label className="ml5 badge badge-primary">BODY</label>
} else {
return <label className="ml5 badge badge-secondary">QUERY</label>
}
}
class SortableTreeTableRow extends Component<any, any> {
render() {
const { property, editable } = this.props
const { handleClickCreateChildPropertyButton, handleDeleteMemoryProperty, handleChangePropertyField, handleSortProperties } = this.props
return (
<RSortable group={property.depth} handle=".SortableTreeTableRow" disabled={!editable} onChange={handleSortProperties}>
<div className={`RSortableWrapper depth${property.depth}`}>
{property.children.sort((a: any, b: any) => a.priority - b.priority).map((item: any) =>
<div key={item.id} className="SortableTreeTableRow" data-id={item.id}>
<div className="flex-row">
{editable &&
<div className="td operations nowrap">
{(item.type === 'Object' || item.type === 'Array')
? <Link
to=""
onClick={e => { e.preventDefault(); handleClickCreateChildPropertyButton(item) }}
>
<GoPlus className="fontsize-14 color-6" />
</Link>
: null}
<Link to="" onClick={e => handleDeleteMemoryProperty(e, item)}><GoTrashcan className="fontsize-14 color-6" /></Link>
</div>
}
<div className={`td payload name depth-${item.depth} nowrap`}>
{!editable
? <span className="nowrap">
{item.name}
{item.scope === 'request' && item.depth === 0 ? <PropertyLabel pos={item.pos} /> : null}
</span>
: <input
value={item.name}
onChange={e => handleChangePropertyField(item.id, 'name', e.target.value)}
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
<div className={`td payload type depth-${item.depth} nowrap`}>
{!editable
? <span className="nowrap">{item.required ? '✔️' : ''}</span>
: <input
type="checkbox"
checked={!!item.required}
onChange={e => handleChangePropertyField(item.id, 'required', e.target.checked)}
/>
}
</div>
<div className="td payload type">
{!editable
? <span className="nowrap">{item.type}</span>
: <select
value={item.type}
onChange={e => handleChangePropertyField(item.id, 'type', e.target.value)}
className="form-control editable"
>
{['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp'].map(type =>
<option key={type} value={type}>{type}</option>
)}
</select>
}
</div>
<div className="td payload rule nowrap">
{!editable
? <span className="nowrap">{item.rule}</span>
: <input
value={item.rule || ''}
onChange={e => handleChangePropertyField(item.id, 'rule', e.target.value)}
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
<div className="td payload value">
{!editable
? <span>{item.value}</span>
: <SmartTextarea
value={item.value || ''}
onChange={(e: any) => handleChangePropertyField(item.id, 'value', e.target.value)}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
<div className="td payload desc">
{!editable
? <span>{item.description}</span>
: <SmartTextarea
value={item.description || ''}
onChange={(e: any) => handleChangePropertyField(item.id, 'description', e.target.value)}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
</div>
{item.children && item.children.length ? <SortableTreeTableRow {...this.props} property={item} /> : null}
</div>
)}
</div>
</RSortable>
)
}
}
class SortableTreeTable extends Component<any, any> {
render() {
const { root, editable } = this.props
return (
<div className={`SortableTreeTable ${editable ? 'editable' : ''}`}>
<SortableTreeTableHeader {...this.props} />
<SortableTreeTableRow {...this.props} property={root} />
</div>
)
}
}
class PropertyList extends Component<any, any> {
static propTypes = {
title: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
scope: PropTypes.string.isRequired,
properties: PropTypes.array,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
/** optional */
bodyOption: PropTypes.string,
requestParamsType: PropTypes.string,
}
constructor(props: any) {
super(props)
this.state = {
createProperty: false,
createChildProperty: false,
previewer: props.scope === 'response',
importer: false,
}
}
render() {
const { title, label, scope, properties = [], repository = {}, mod = {}, itf = {} } = this.props
if (!itf.id) { return null }
const { editable, requestParamsType } = this.props // itf.locker && (itf.locker.id === auth.id)
const pos = rptFromStr2Num(requestParamsType)
let scopedProperties = properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === scope)
if (scope === 'request' && editable) {
scopedProperties = scopedProperties.filter((s: any) => s.pos === pos)
}
return (
<section className="PropertyList">
<div className="header clearfix">
<span className="title">{title || `${label}属性`}</span>
<div className="toolbar">
<ButtonGroup size="small" color="primary">
{editable && [
<Button key={1} onClick={this.handleClickCreatePropertyButton}></Button>,
<Button key={2} onClick={this.handleClickImporterButton}></Button>,
]}
<Button onClick={this.handleClickPreviewerButton}>
</Button>
</ButtonGroup>
</div>
</div>
<div className="body">
<SortableTreeTable
root={Tree.arrayToTree(scopedProperties)}
editable={editable}
handleClickCreateChildPropertyButton={this.handleClickCreateChildPropertyButton}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
handleChangePropertyField={this.handleChangePropertyField}
handleSortProperties={this.handleSortProperties}
/>
</div>
<div className="footer">
{this.state.previewer && <Previewer scope={scope} label={label} properties={properties} itf={itf} />}
</div>
<RModal
when={this.state.createProperty}
onClose={() => this.setState({ createProperty: false })}
onResolve={this.handleCreatePropertySucceeded}
>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} />
</RModal>
<RModal
when={!!this.state.createChildProperty}
onClose={() => this.setState({ createChildProperty: false })}
onResolve={this.handleCreatePropertySucceeded}
>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} parent={this.state.createChildProperty} />
</RModal>
<RModal when={this.state.importer} onClose={() => this.setState({ importer: false })} onResolve={this.handleCreatePropertySucceeded}>
<Importer title={`导入${label}属性`} repository={repository} mod={mod} itf={itf} scope={scope} />
</RModal>
</section>
)
}
handleClickCreatePropertyButton = () => {
this.setState({ createProperty: true })
}
handleClickCreateChildPropertyButton = (item: any) => {
this.setState({ createChildProperty: item })
}
handleClickImporterButton = () => {
this.setState({ importer: true })
}
handleClickPreviewerButton = () => {
this.setState({ previewer: !this.state.previewer })
}
handleChangePropertyField = (id: any, key: any, value: any) => {
const { handleChangeProperty } = this.context
const { properties } = this.props
const property = properties.find((property: any) => property.id === id)
handleChangeProperty({ ...property, [key]: value })
}
handleCreatePropertySucceeded = () => {
/** empty */
}
handleDeleteMemoryProperty = (e: any, property: any) => {
e.preventDefault()
const { handleDeleteMemoryProperty } = this.context
handleDeleteMemoryProperty(property)
}
handleSortProperties = (_: any, sortable: any) => {
const { properties } = this.props
const ids = sortable.toArray()
ids.forEach((id: any, index: any) => {
const property = properties.find((item: any) => item.id === id || item.id === +id)
property.priority = index + 1
})
}
}
export default PropertyList

@ -82,7 +82,7 @@
margin-right: 1rem;
.ModuleList
margin: 0 0 1.5rem 0;
margin: 0;
padding: 0 2rem;
list-style: none;
border-bottom: 1px solid #e1e4e8;
@ -130,7 +130,8 @@
.InterfaceWrapper
display: flex;
flex-direction: row;
padding: 0 2rem;
background-color: #FFFFFF;
padding: 2rem;
.InterfaceList
flex-basis: 16rem;
flex-shrink: 0;

@ -12,10 +12,11 @@ import { addRepository, updateRepository, clearRepository, fetchRepository } fro
import { addModule, updateModule, deleteModule, sortModuleList } from '../../actions/module'
import { addInterface, updateInterface, deleteInterface, lockInterface, unlockInterface, sortInterfaceList } from '../../actions/interface'
import { addProperty, updateProperty, deleteProperty, updateProperties, sortPropertyList } from '../../actions/property'
import { GoRepo, GoPencil, GoPlug, GoDatabase, GoJersey, GoLinkExternal } from 'react-icons/lib/go'
import { GoRepo, GoPencil, GoPlug, GoDatabase, GoJersey, GoLinkExternal } from 'react-icons/go'
import './RepositoryEditor.css'
import ExportPostmanForm from '../repository/ExportPostmanForm'
import { RootState } from 'actions/types'
// DONE 2.1 import Spin from '../utils/Spin'
// TODO 2.2 缺少测试器
@ -23,15 +24,12 @@ import ExportPostmanForm from '../repository/ExportPostmanForm'
// TODO 2.1 大数据测试,含有大量模块、接口、属性的仓库
// 展示组件
class RepositoryEditor extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
class RepositoryEditor extends Component<any, any> {
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
onClearRepository: PropTypes.func.isRequired
onClearRepository: PropTypes.func.isRequired,
}
static childContextTypes = {
onAddRepository: PropTypes.func.isRequired,
@ -50,7 +48,15 @@ class RepositoryEditor extends Component {
onUpdateProperty: PropTypes.func.isRequired,
onUpdateProperties: PropTypes.func.isRequired,
onDeleteProperty: PropTypes.func.isRequired,
onSortPropertyList: PropTypes.func.isRequired
onSortPropertyList: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = {
update: false,
exportPostman: false,
}
}
getChildContext() {
return _.pick(this.props, Object.keys(RepositoryEditor.childContextTypes))
@ -62,71 +68,68 @@ class RepositoryEditor extends Component {
this.props.onFetchRepository({ id })
}
}
constructor(props) {
super(props)
this.state = {
update: false,
exportPostman: false
}
}
render() {
let { location: { params }, auth, repository } = this.props
if (repository.data.name) {
document.title = `RAP2 ${repository.data.name}`
}
if (!repository.fetching && !repository.data) return <div className='p100 fontsize-40 text-center'>404</div>
const { location: { params }, auth } = this.props
let { repository } = this.props
if (!repository.fetching && !repository.data) { return <div className="p100 fontsize-40 text-center">404</div> }
repository = repository.data
if (!repository.id) return <Spin /> // // DONE 2.2 每次获取仓库都显示加载动画不合理,应该只在初始加载时显示动画。
if (repository.name) {
document.title = `RAP2 ${repository.name}`
}
if (!repository.id) { return <Spin /> } // // DONE 2.2 每次获取仓库都显示加载动画不合理,应该只在初始加载时显示动画。
let mod = repository && repository.modules && repository.modules.length
? (repository.modules.find(item => item.id === +params.mod) || repository.modules[0]) : {}
let itf = mod.interfaces && mod.interfaces.length
? (mod.interfaces.find(item => item.id === +params.itf) || mod.interfaces[0]) : {}
let properties = itf.properties || []
const mod = repository && repository.modules && repository.modules.length
? (repository.modules.find((item: any) => item.id === +params.mod) || repository.modules[0]) : {}
const itf = mod.interfaces && mod.interfaces.length
? (mod.interfaces.find((item: any) => item.id === +params.itf) || mod.interfaces[0]) : {}
const properties = itf.properties || []
let ownerlink = repository.organization
const ownerlink = repository.organization
? `/organization/repository?organization=${repository.organization.id}`
: `/repository/joined?user=${repository.owner.id}`
let isOwned = repository.owner.id === auth.id
let isJoined = repository.members.find(itme => itme.id === auth.id)
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((item: any) => item.id === auth.id)
return (
<article className='RepositoryEditor'>
<div className='header'>
<span className='title'>
<GoRepo className='mr6 color-9' />
<article className="RepositoryEditor">
<div className="header">
<span className="title">
<GoRepo className="mr6 color-9" />
<Link to={`${ownerlink}`}>{repository.organization ? repository.organization.name : repository.owner.fullname}</Link>
<span className='slash'> / </span>
<span className="slash"> / </span>
<span>{repository.name}</span>
</span>
<div className='toolbar'>
<div className="toolbar">
{/* 编辑权限:拥有者或者成员 */}
{isOwned || isJoined
? <span className='fake-link edit' onClick={e => this.setState({ update: true })}><GoPencil /> </span>
? <span className="fake-link edit" onClick={() => this.setState({ update: true })}><GoPencil /> </span>
: null
}
<RModal when={this.state.update} onClose={e => this.setState({ update: false })} onResolve={this.handleUpdate}>
<RepositoryForm title='编辑仓库' repository={repository} />
<RModal when={this.state.update} onClose={() => this.setState({ update: false })} onResolve={this.handleUpdate}>
<RepositoryForm title="编辑仓库" repository={repository} />
</RModal>
<a href={`${serve}/app/plugin/${repository.id}`} target='_blank' className='api'><GoPlug /> </a>
<a href={`${serve}/repository/get?id=${repository.id}`} target='_blank' className='api'><GoDatabase /> </a>
<a href={`${serve}/test/test.plugin.jquery.html?id=${repository.id}`} target='_blank' className='api'><GoJersey /> </a>
<span className='fake-link edit' onClick={e => this.setState({ exportPostman: true })}><GoLinkExternal /> Postman Collection</span>
<RModal when={this.state.exportPostman} onClose={e => this.setState({ exportPostman: false })} onResolve={e => this.setState({ exportPostman: false })}>
<ExportPostmanForm title='导出到Postman' repoId={repository.id} />
<a href={`${serve}/app/plugin/${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoPlug /> </a>
<a href={`${serve}/repository/get?id=${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoDatabase /> </a>
<a href={`${serve}/test/test.plugin.jquery.html?id=${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoJersey /> </a>
<span className="fake-link edit" onClick={() => this.setState({ exportPostman: true })}><GoLinkExternal /> Postman Collection</span>
<RModal
when={this.state.exportPostman}
onClose={() => this.setState({ exportPostman: false })}
onResolve={() => this.setState({ exportPostman: false })}
>
<ExportPostmanForm title="导出到Postman" repoId={repository.id} />
</RModal>
</div>
<RepositorySearcher repository={repository} />
<div className='desc'>{repository.description}</div>
<div className="desc">{repository.description}</div>
<DuplicatedInterfacesWarning repository={repository} />
</div>
<div className='body'>
<div className="body">
<ModuleList mods={repository.modules} repository={repository} mod={mod} />
<div className='InterfaceWrapper'>
<div className="InterfaceWrapper">
<InterfaceList itfs={mod.interfaces} repository={repository} mod={mod} itf={itf} />
<InterfaceEditor itf={itf} properties={properties} mod={mod} repository={repository} />
</div>
@ -134,20 +137,17 @@ class RepositoryEditor extends Component {
</article>
)
}
handleUpdate = (e) => {
let { store } = this.context
let { pathname, hash, search } = store.getState().router.location
store.dispatch(replace(pathname + search + hash))
}
componentWillUnmount() {
// this.props.onClearRepository()
handleUpdate = () => {
const { pathname, hash, search } = this.props.router.location
this.props.replace(pathname + search + hash)
}
}
// 容器组件
const mapStateToProps = (state) => ({
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
repository: state.repository
repository: state.repository,
router: state.router,
})
const mapDispatchToProps = ({
onFetchRepository: fetchRepository,
@ -168,7 +168,8 @@ const mapDispatchToProps = ({
onUpdateProperty: updateProperty,
onUpdateProperties: updateProperties,
onDeleteProperty: deleteProperty,
onSortPropertyList: sortPropertyList
onSortPropertyList: sortPropertyList,
replace,
})
export default connect(
mapStateToProps,

@ -1,132 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, Link, StoreStateRouterLocationURI, URI } from '../../family'
class Highlight extends Component {
static replace = (clip, seed) => {
if (!seed) return clip
let rseed = new RegExp(seed, 'ig')
return ('' + clip).replace(rseed, (matched) =>
`<span class='highlight'>${matched}</span>`
)
}
render () {
let { clip, seed } = this.props
let highlighted = { __html: Highlight.replace(clip, seed) }
return (
<span {...this.props} dangerouslySetInnerHTML={highlighted} />
)
}
}
class DropdownMenu extends Component {
static filter = (respository, seed) => {
let nextRespository = { ...respository, modules: [] }
let counter = 0
respository.modules.forEach(mod => {
let nextModule = { ...mod, interfaces: [] }
let matchModule = nextModule.name.indexOf(seed) !== -1
if (matchModule) {
counter++
nextRespository.modules.push(nextModule)
}
mod.interfaces.forEach(itf => {
let nextInterface = { ...itf, properties: [] }
let matchInterface = nextInterface.name.indexOf(seed) !== -1 || nextInterface.url.indexOf(seed) !== -1 || nextInterface.method === seed
if (matchInterface) {
counter++
if (!matchModule) {
matchModule = true
nextRespository.modules.push(nextModule)
}
nextModule.interfaces.push(nextInterface)
}
itf.properties.forEach(property => {
let nextProperty = { ...property }
let matchProperty = nextProperty.name.indexOf(seed) !== -1
if (matchProperty) {
counter++
if (!matchModule) {
matchModule = true
nextRespository.modules.push(nextModule)
}
if (!matchInterface) {
matchInterface = true
nextModule.interfaces.push(nextInterface)
}
nextInterface.properties.push(nextProperty)
}
})
})
})
return { nextRespository, counter }
}
static highlight = (clip, seed) => {
if (!seed) return clip
let rseed = new RegExp(seed, 'ig')
return ('' + clip).replace(rseed, (matched) =>
`<span class='highlight'>${matched}</span>`
)
}
static contextTypes = {
store: PropTypes.object
}
render () {
let { repository, seed, onSelect } = this.props
let uri = StoreStateRouterLocationURI(this.context.store).removeSearch('mod').removeSearch('itf')
let { nextRespository, counter } = DropdownMenu.filter(repository, seed)
if (counter === 0) return null
return (
<div className='dropdown-menu'>
{nextRespository.modules.map((mod, index, modules) =>
<div key={`mod-${mod.id}`}>
<Link to={URI(uri).setSearch({ mod: mod.id }).href()} onClick={onSelect} className='dropdown-item dropdown-item-module'>
<span className='label'>模块</span>
<Highlight className='dropdown-item-clip' clip={mod.name} seed={seed} />
</Link>
{mod.interfaces.map(itf =>
<div key={`itf-${itf.id}`} >
<Link to={URI(uri).setSearch({ mod: itf.moduleId }).setSearch({ itf: itf.id }).href()} onClick={onSelect} className='dropdown-item dropdown-item-interface'>
<span className='label'>接口</span>
<Highlight className='dropdown-item-clip' clip={itf.name} seed={seed} />
<Highlight className='dropdown-item-clip' clip={itf.method} seed={seed} />
<Highlight className='dropdown-item-clip' clip={itf.url} seed={seed} />
</Link>
{itf.properties.map(property =>
<Link key={`property-${property.id}`} to={URI(uri).setSearch({ mod: property.moduleId }).setSearch({ itf: property.interfaceId }).href()} onClick={onSelect} className='dropdown-item dropdown-item-property'>
<span className='label'>属性</span>
<Highlight className='dropdown-item-clip' clip={property.name} seed={seed} />
</Link>
)}
</div>
)}
{index < modules.length - 1 && <div className='dropdown-divider' />}
</div>
)}
</div>
)
}
}
// TODO 2.2
class RepositorySearcher extends Component {
constructor (props) {
super(props)
this.state = { seed: '' }
}
render () {
let { repository } = this.props
return (
<div className='RepositorySearcher dropdown'>
<input value={this.state.seed} onChange={e => { this.setState({ seed: e.target.value }) }} className='dropdown-input form-control' placeholder='工作区搜索' />
{this.state.seed && <DropdownMenu repository={repository} seed={this.state.seed} onSelect={this.clearSeed} />}
</div>
)
}
clearSeed = (e) => {
this.setState({ seed: '' })
}
}
export default RepositorySearcher

@ -0,0 +1,145 @@
import React, { Component } from 'react'
import { PropTypes, Link, StoreStateRouterLocationURI, connect } from '../../family'
class Highlight extends Component<any, any> {
static replace = (clip: any, seed: any) => {
if (!seed) { return clip }
const rseed = new RegExp(seed, 'ig')
return ('' + clip).replace(rseed, (matched) =>
`<span class='highlight'>${matched}</span>`
)
}
render() {
const { clip, seed } = this.props
const highlighted = { __html: Highlight.replace(clip, seed) }
return (
<span {...this.props} dangerouslySetInnerHTML={highlighted} />
)
}
}
class DropdownMenuBase extends Component<any, any> {
static contextTypes = {
store: PropTypes.object,
}
static filter = (respository: any, seed: any) => {
const nextRespository = { ...respository, modules: [] }
let counter = 0
respository.modules.forEach((mod: any) => {
const nextModule = { ...mod, interfaces: [] }
let matchModule = nextModule.name.indexOf(seed) !== -1
if (matchModule) {
counter++
nextRespository.modules.push(nextModule)
}
mod.interfaces.forEach((itf: any) => {
const nextInterface = { ...itf, properties: [] }
let matchInterface = nextInterface.name.indexOf(seed) !== -1 || nextInterface.url.indexOf(seed) !== -1 || nextInterface.method === seed
if (matchInterface) {
counter++
if (!matchModule) {
matchModule = true
nextRespository.modules.push(nextModule)
}
nextModule.interfaces.push(nextInterface)
}
itf.properties.forEach((property: any) => {
const nextProperty = { ...property }
const matchProperty = nextProperty.name.indexOf(seed) !== -1
if (matchProperty) {
counter++
if (!matchModule) {
matchModule = true
nextRespository.modules.push(nextModule)
}
if (!matchInterface) {
matchInterface = true
nextModule.interfaces.push(nextInterface)
}
nextInterface.properties.push(nextProperty)
}
})
})
})
return { nextRespository, counter }
}
static highlight = (clip: any, seed: any) => {
if (!seed) { return clip }
const rseed = new RegExp(seed, 'ig')
return ('' + clip).replace(rseed, (matched) =>
`<span class='highlight'>${matched}</span>`
)
}
render() {
const { repository, seed, onSelect, router } = this.props
const uri = StoreStateRouterLocationURI(router).removeSearch('mod').removeSearch('itf')
const { nextRespository, counter } = DropdownMenu.filter(repository, seed)
if (counter === 0) { return null }
return (
<div className="dropdown-menu">
{nextRespository.modules.map((mod: any, index: any, modules: any) =>
<div key={`mod-${mod.id}`}>
<Link to={uri.setSearch({ mod: mod.id }).href()} onClick={onSelect} className="dropdown-item dropdown-item-module">
<span className="label"></span>
<Highlight className="dropdown-item-clip" clip={mod.name} seed={seed} />
</Link>
{mod.interfaces.map((itf: any) =>
<div key={`itf-${itf.id}`} >
<Link
to={uri.setSearch({ mod: itf.moduleId }).setSearch({ itf: itf.id }).href()}
onClick={onSelect}
className="dropdown-item dropdown-item-interface"
>
<span className="label"></span>
<Highlight className="dropdown-item-clip" clip={itf.name} seed={seed} />
<Highlight className="dropdown-item-clip" clip={itf.method} seed={seed} />
<Highlight className="dropdown-item-clip" clip={itf.url} seed={seed} />
</Link>
{itf.properties.map((property: any) =>
<Link key={`property-${property.id}`} to={uri.setSearch({ mod: property.moduleId }).setSearch({ itf: property.interfaceId }).href()} onClick={onSelect} className="dropdown-item dropdown-item-property">
<span className="label"></span>
<Highlight className="dropdown-item-clip" clip={property.name} seed={seed} />
</Link>
)}
</div>
)}
{index < modules.length - 1 && <div className="dropdown-divider" />}
</div>
)}
</div>
)
}
}
const DropdownMenu = connect((state: any) => ({ router: state.router }))(DropdownMenuBase)
// TODO 2.2 自动隐藏,高阶组件
class RepositorySearcher extends Component<any, any> {
constructor(props: any) {
super(props)
this.state = { seed: '' }
}
render() {
const { repository } = this.props
return (
<div className="RepositorySearcher dropdown">
<input
value={this.state.seed}
onChange={e => { this.setState({ seed: e.target.value }) }}
className="dropdown-input form-control"
placeholder="工作区搜索"
/>
{this.state.seed && <DropdownMenu repository={repository} seed={this.state.seed} onSelect={this.clearSeed} />}
</div>
)
}
clearSeed = () => {
this.setState({ seed: '' })
}
}
export default connect((state: any) => ({
router: state.router,
}))(RepositorySearcher)

@ -1,53 +0,0 @@
import React from 'react'
import { connect, Link } from '../../family'
import { Spin } from '../utils'
import OwnedRepositoriesCard from './OwnedRepositoriesCard'
import JoinedRepositoriesCard from './JoinedRepositoriesCard'
import LogsCard from './LogsCard'
import './Home.css'
import { GoRepo } from 'react-icons/lib/go'
const Maiden = () => (
<div className='Maiden'>
<Link to='/repository/joined/create' className=' btn btn-lg btn-success'><GoRepo /> 新建仓库</Link>
</div>
)
//
const Home = ({ auth, owned, joined, logs }) => {
if (owned.fetching || joined.fetching || logs.fetching) return <Spin />
if (!owned.data.length && !joined.data.length) {
return (
<div className='Home'>
<Maiden />
</div>
)
}
return (
<div className='Home'>
<div className='row'>
<div className='col-12 col-sm-8 col-md-8 col-lg-8'>
<LogsCard logs={logs} />
</div>
<div className='col-12 col-sm-4 col-md-4 col-lg-4'>
<OwnedRepositoriesCard repositories={owned} />
<JoinedRepositoriesCard repositories={joined} />
</div>
</div>
</div>
)
}
//
const mapStateToProps = (state) => ({
auth: state.auth,
owned: state.ownedRepositories,
joined: state.joinedRepositories,
logs: state.logs
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home)

@ -0,0 +1,56 @@
import React from 'react'
import { Link } from '../../family'
import { connect } from 'react-redux'
import { Spin } from '../utils'
import OwnedRepositoriesCard from './OwnedRepositoriesCard'
import JoinedRepositoriesCard from './JoinedRepositoriesCard'
import LogsCard from './LogsCard'
import './Home.css'
import { GoRepo } from 'react-icons/go'
import { RootState } from 'actions/types'
const Maiden = () => (
<div className="Maiden">
<Link to="/repository/joined/create" className=" btn btn-lg btn-success"><GoRepo /> </Link>
</div>
)
// 展示组件
const Home = ({ owned, joined, logs }: any) => {
if (owned.fetching || joined.fetching || logs.fetching) { return <Spin /> }
if (!owned.data.length && !joined.data.length) {
return (
<div className="Home">
<Maiden />
</div>
)
}
return (
<div className="Home">
<div className="row">
<div className="col-12 col-sm-8 col-md-8 col-lg-8">
<LogsCard logs={logs} />
</div>
<div className="col-12 col-sm-4 col-md-4 col-lg-4">
<OwnedRepositoriesCard repositories={owned} />
<div style={{ marginTop: 8 }}>
<JoinedRepositoriesCard repositories={joined} />
</div>
</div>
</div>
</div>
)
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
owned: state.ownedRepositories,
joined: state.joinedRepositories,
logs: state.logs,
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home)

@ -1,25 +1,26 @@
import React from 'react'
import { Link } from '../../family'
import { Spin } from '../utils'
import { Card } from '@material-ui/core'
const JoinedRepositoriesCard = ({ repositories }) => (
<div className='card'>
<div className='card-header'></div>
const JoinedRepositoriesCard = ({ repositories }: any) => (
<Card>
<div className="card-header"></div>
{repositories.fetching ? <Spin /> : (
<div className='card-block'>
{repositories.data.slice(0, 10).map(repository =>
<div className="card-block">
{repositories.data.slice(0, 10).map((repository: any) =>
<p key={repository.id}><JoinedRepositoryLink repository={repository} /></p>
)}
{repositories.data.length === 0 ? <span>-</span> : null}
{repositories.data.length > 10
? <Link to='/repository/joined'>=> {repositories.data.length} </Link>
? <Link to="/repository/joined">=> {repositories.data.length} </Link>
: null
}
</div>)
}
</div>
</Card>
)
const JoinedRepositoryLink = ({ repository }) => (
const JoinedRepositoryLink = ({ repository }: any) => (
<Link to={`/repository/editor?id=${repository.id}`}>
<span>{repository.organization ? repository.organization.name : repository.owner.fullname}</span>
<span> / </span>

@ -1,6 +1,7 @@
import React from 'react'
import { Link, moment } from '../../family'
import { Spin } from '../utils'
import { Card } from '@material-ui/core'
// DONE 2.3 重构 LogView
// 1. √ 旧逻辑把 targe 和 type 混在一起,各种判断看的好蛋疼
@ -10,56 +11,56 @@ import { Spin } from '../utils'
// 2. 墨智邀请麦少加入群聊
// 4. √ 用户头像应该提成组件 UserAvatar而不是混在 HTML 代码里
// 5. √ FromNow 应该提成组件
const UserAvatar = ({ user }) => (
const UserAvatar = ({ user }: any) => (
user
? <img alt={user.empId} src={`https://work.alibaba-inc.com/photo/${user.empId}.220x220.jpg`} className='Log-avatar' />
? <img alt={user.empId} src={`https://work.alibaba-inc.com/photo/${user.empId}.220x220.jpg`} className="Log-avatar" />
: null
)
const UserLink = ({ user }) => (
<Link to={`https://work.alibaba-inc.com/work/u/${user.empId}`} target='_blank' className='Log-user-link'>{user.fullname}</Link>
const UserLink = ({ user }: any) => (
<Link to={`https://work.alibaba-inc.com/work/u/${user.empId}`} target="_blank" className="Log-user-link">{user.fullname}</Link>
)
const LogUserView = ({ user }) => {
const LogUserView = ({ user }: any) => {
return (
<span className='Log-user'>
<span className="Log-user">
<UserAvatar user={user} />
<UserLink user={user} />
</span>
)
}
const LogTypeView = ({ type }) => {
let typeName = {
const LogTypeView = ({ type }: any) => {
const typeName: any = ({
create: '创建了',
update: '修改了',
delete: '删除了',
lock: '锁定了',
unlock: '释放了',
join: '加入了',
exit: '退出了'
}[type]
return <span className='Log-type'>{typeName}</span>
exit: '退出了',
} as any)[type]
return <span className="Log-type">{typeName}</span>
}
const LogTargetView = ({ log }) => {
let targetType = (log.organization && 'organization') || // 团队
const LogTargetView = ({ log }: any) => {
const targetType = (log.organization && 'organization') || // 团队
(log.repository && !log.module && !log.interface && 'repository') || // 仓库
(log.repository && log.module && !log.interface && 'module') || // 模块
(log.repository && log.module && log.interface && 'interface') // 接口
switch (targetType) {
switch (targetType as any) {
case 'organization':
return !log.organization.deletedAt
? <span className='Log-target'><Link to={`/organization/repository?organization=${log.organization.id}`}>{log.organization.name}</Link></span>
? <span className="Log-target"><Link to={`/organization/repository?organization=${log.organization.id}`}>{log.organization.name}</Link></span>
: <s>{log.organization.name}</s>
case 'repository':
return !log.repository.deletedAt
? <span className='Log-target'><Link to={`/repository/editor?id=${log.repository.id}`}>{log.repository.name}</Link></span>
? <span className="Log-target"><Link to={`/repository/editor?id=${log.repository.id}`}>{log.repository.name}</Link></span>
: <s>{log.repository.name}</s>
case 'module':
return (
<span className='Log-target'>
<span className="Log-target">
{!log.repository.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}`}>{log.repository.name}</Link>
: <s>{log.repository.name}</s>
}
<span className='slash'> / </span>
<span className="slash"> / </span>
{!log.module.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}&mod=${log.module.id}`}>{log.module.name}</Link>
: <s>{log.module.name}</s>
@ -68,17 +69,17 @@ const LogTargetView = ({ log }) => {
)
case 'interface':
return (
<span className='Log-target'>
<span className="Log-target">
{!log.repository.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}`}>{log.repository.name}</Link>
: <s>{log.repository.name}</s>
}
<span className='slash'> / </span>
<span className="slash"> / </span>
{!log.module.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}&mod=${log.module.id}`}>{log.module.name}</Link>
: <s>{log.module.name}</s>
}
<span className='slash'> / </span>
<span className="slash"> / </span>
{!log.interface.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}&mod=${log.module.id}&itf=${log.interface.id}`}>{log.interface.name}</Link>
: <s>{log.interface.name}</s>
@ -86,27 +87,27 @@ const LogTargetView = ({ log }) => {
</span>
)
default:
return <div></div>
return <div />
}
}
const FromNow = ({ date }) => (
<i className='Log-fromnow'>{moment(date).fromNow()}</i>
const FromNow = ({ date }: any) => (
<i className="Log-fromnow">{moment(date).fromNow()}</i>
)
const LogView = ({ log }) => {
const LogView = ({ log }: any) => {
if (log.creator && /join|exit/.test(log.type) && log.creator.id !== log.user.id) {
// if (log.creator.id === log.user.id) return null
if (log.type === 'join') return <JoinLogView log={log} />
if (log.type === 'exit') return <ExitLogView log={log} />
if (log.type === 'join') { return <JoinLogView log={log} /> }
if (log.type === 'exit') { return <ExitLogView log={log} /> }
}
return (
<div className='Log clearfix'>
<div className='Log-body'>
<div className="Log clearfix">
<div className="Log-body">
<LogUserView user={log.user} />
<LogTypeView type={log.type} />
<LogTargetView log={log} />
</div>
<div className='Log-footer'>
<div className="Log-footer">
<FromNow date={log.createdAt} />
</div>
</div>
@ -115,45 +116,45 @@ const LogView = ({ log }) => {
// DONE 2.3 支持『上帝之手』`log.creator`
// 墨智邀请麦少加入群聊
const JoinLogView = ({ log }) => {
const JoinLogView = ({ log }: any) => {
return (
<div className='Log clearfix'>
<div className='Log-body'>
<div className="Log clearfix">
<div className="Log-body">
<LogUserView user={log.creator} />
<span className='Log-type'></span>
<span className="Log-type"></span>
<UserLink user={log.user} />
<span className='Log-type'></span>
<span className="Log-type"></span>
<LogTargetView log={log} />
</div>
<div className='Log-footer'>
<div className="Log-footer">
<FromNow date={log.createdAt} />
</div>
</div>
)
}
// 墨智将麦少移出群聊
const ExitLogView = ({ log }) => {
const ExitLogView = ({ log }: any) => {
return (
<div className='Log clearfix'>
<div className='Log-body'>
<div className="Log clearfix">
<div className="Log-body">
<LogUserView user={log.creator} />
<span className='Log-type'></span>
<span className="Log-type"></span>
<UserLink user={log.user} />
<span className='Log-type'></span>
<span className="Log-type"></span>
<LogTargetView log={log} />
</div>
<div className='Log-footer'>
<div className="Log-footer">
<FromNow date={log.createdAt} />
</div>
</div>
)
}
const Log = ({ log }) => { // eslint-disable-line no-unused-vars
const userAvatar = <img alt={log.user.empId} src={`https://work.alibaba-inc.com/photo/${log.user.empId}.220x220.jpg`} className='avatar' />
const userLink = <Link to={`https://work.alibaba-inc.com/work/u/${log.user.empId}`} target='_blank'>{log.user.fullname}</Link>
const fromNow = <i className='fromnow'>{moment(log.updatedAt).fromNow()}</i>
let targetName, targetLink, typeName
export const Log = ({ log }: any) => {
const userAvatar = <img alt={log.user.empId} src={`https://work.alibaba-inc.com/photo/${log.user.empId}.220x220.jpg`} className="avatar" />
const userLink = <Link to={`https://work.alibaba-inc.com/work/u/${log.user.empId}`} target="_blank">{log.user.fullname}</Link>
const fromNow = <i className="fromnow">{moment(log.updatedAt).fromNow()}</i>
let targetName: any, targetLink: any, typeName: any
if (log.organization) { // 团队
targetName = log.organization.name
targetLink = !log.organization.deletedAt
@ -180,49 +181,56 @@ const Log = ({ log }) => { // eslint-disable-line no-unused-vars
}
switch (log.type) {
case 'create':
return targetLink ? <div className='Log clearfix'>
{userAvatar} <span className='body'>{userLink} <span className='ml6 mr6'></span> {targetLink}</span> {fromNow}
</div> : null
return targetLink ? (
<div className="Log clearfix">
{userAvatar} <span className="body">{userLink} <span className="ml6 mr6"></span> {targetLink}</span> {fromNow}
</div>
) : null
case 'update':
return targetLink ? <div className='Log clearfix'>
{userAvatar} <span className='body'>{userLink} <span className='ml6 mr6'></span> {targetLink}</span> {fromNow}
</div> : null
return targetLink ? (
<div className="Log clearfix">
{userAvatar} <span className="body">{userLink} <span className="ml6 mr6"></span> {targetLink}</span> {fromNow}
</div>
) : null
case 'delete':
return targetName ? <div className='Log clearfix'>
{userAvatar} <span className='body'>{userLink} <span className='ml6 mr6'></span> <s>{targetName}</s></span> {fromNow}
</div> : null
return targetName ? (
<div className="Log clearfix">
{userAvatar} <span className="body">{userLink} <span className="ml6 mr6"></span> <s>{targetName}</s></span> {fromNow}
</div>
) : null
case 'lock':
case 'unlock':
typeName = { lock: '锁定', unlock: '释放' }[log.type]
if (!log.repository || !log.module || !log.interface) return null
return <div className='Log clearfix'>
{userAvatar} <span className='body'>{userLink} <span className='ml6 mr6'>{typeName}</span> {targetLink}</span> {fromNow}
</div>
typeName = ({ lock: '锁定', unlock: '释放' } as any)[log.type]
if (!log.repository || !log.module || !log.interface) { return null }
return (
<div className="Log clearfix">
{userAvatar} <span className="body">{userLink} <span className="ml6 mr6">{typeName}</span> {targetLink}</span> {fromNow}
</div>
)
case 'join':
case 'exit':
typeName = { join: '加入', exit: '退出' }[log.type]
return targetName ? <div className='Log clearfix'>
{userAvatar} <span className='body'>{userLink} <span className='ml6 mr6'>{typeName}</span> {targetLink}</span> {fromNow}
</div> : null
typeName = ({ join: '加入', exit: '退出' } as any)[log.type]
return targetName ? (
<div className="Log clearfix">
{userAvatar} <span className="body">{userLink} <span className="ml6 mr6">{typeName}</span> {targetLink}</span> {fromNow}
</div>
) : null
default:
return null
// return <p>{JSON.stringify(log)}</p>
// return <p>{JSON.stringify(log)}</p>
}
}
const LogsCard = ({ logs }) => (
<div className='Logs card'>
{/* DONE 2.1 不屏蔽首页 */}
{/* DONE 2.2 √用户、√团队、√仓库的动态(简) */}
{/* DONE 2.2 团队-仓库、团队-用户的动态 */}
{/* <div className='card-header'>React + Redux + Router + Saga</div> */}
const LogsCard = ({ logs }: any) => (
<Card className="Logs card">
<div className="card-header"></div>
{logs.fetching ? <Spin /> : (
<div className='card-block'>
{logs.data.map(log =>
<div className="card-block">
{logs.data.map((log: any) =>
<LogView key={log.id} log={log} />
)}
</div>
)}
</div>
</Card>
)
export default LogsCard

@ -1,25 +1,26 @@
import React from 'react'
import { Link } from '../../family'
import { Spin } from '../utils'
import { Card } from '@material-ui/core'
const OwnedRepositoriesCard = ({ repositories }) => (
<div className='card'>
<div className='card-header'></div>
const OwnedRepositoriesCard = ({ repositories }: any) => (
<Card>
<div className="card-header"></div>
{repositories.fetching ? <Spin /> : (
<div className='card-block'>
{repositories.data.slice(0, 10).map(repository =>
<div className="card-block">
{repositories.data.slice(0, 10).map((repository: any) =>
<p key={repository.id}><OwnedRepositoryLink repository={repository} /></p>
)}
{repositories.data.length === 0 ? <span>-</span> : null}
{repositories.data.length > 10
? <Link to='/repository/joined'>=> {repositories.data.length} </Link>
? <Link to="/repository/joined">=> {repositories.data.length} </Link>
: null
}
</div>)
}
</div>
</Card>
)
const OwnedRepositoryLink = ({ repository }) => (
const OwnedRepositoryLink = ({ repository }: any) => (
<Link to={`/repository/editor?id=${repository.id}`}>
<span>{repository.organization ? repository.organization.name + ' / ' : ''}</span>
<span>{repository.name}</span>

File diff suppressed because one or more lines are too long

@ -0,0 +1,72 @@
import React from 'react'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Button from '@material-ui/core/Button'
import { Link } from 'react-router-dom'
import { User } from 'actions/types'
import Logo from './Logo'
import { useDispatch } from 'react-redux'
import { logout } from 'actions/account'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
width: '100%',
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
link: {
color: '#FFFFFF',
'&:hover': {
color: '#FFFFFF',
},
},
right: {
float: 'right',
},
toolbar: {
display: 'flex',
justifyContent: 'space-between',
},
logo: {
marginRight: theme.spacing(2),
display: 'inline',
},
})
)
interface Props {
user: User
}
export default function MainMenu(props: Props) {
const { user } = props
const classes = useStyles()
const dispatch = useDispatch()
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar className={classes.toolbar}>
<div>
<div className={classes.logo}> <Logo /> </div>
<Link to="/" className={classes.link}><Button color="inherit"> </Button></Link>
<Link to="/repository/joined" className={classes.link}><Button color="inherit"> </Button></Link>
<Link to="/organization/joined" className={classes.link}><Button color="inherit"> </Button></Link>
<Link to="/api" className={classes.link}><Button color="inherit"> </Button></Link>
<Link to="/status" className={classes.link}><Button color="inherit"> </Button></Link>
</div>
<div>
{user.id && <Button color="inherit" onClick={() => dispatch(logout())}></Button>}
</div>
</Toolbar>
</AppBar>
</div>
)
}

@ -2,25 +2,26 @@ import React, { Component } from 'react'
import { connect } from 'react-redux'
import { contextTypes, childContextTypes, getChildContext, CreateButton, OrganizationsTypeDropdown, SearchGroup, OrganizationListWithSpin, PaginationWithLocation, mapDispatchToProps } from './OrganizationListParts'
import './Organization.css'
import { RootState } from 'actions/types'
// 所有团队
class JoinedOrganizationList extends Component {
class JoinedOrganizationList extends Component<any, any> {
static contextTypes = contextTypes
static childContextTypes = childContextTypes
getChildContext = getChildContext
render () {
let { location, match, organizations } = this.props
render() {
const { location, match, organizations } = this.props
return (
<section className='OrganizationListWrapper'>
<nav className='toolbar clearfix'>
<section className="OrganizationListWrapper">
<nav className="toolbar clearfix">
<OrganizationsTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
<CreateButton />
</nav>
<div className='body'>
<div className="body">
<OrganizationListWithSpin name={location.params.name} organizations={organizations} />
</div>
<div className='footer'>
<div className="footer">
<PaginationWithLocation calculated={organizations.pagination} />
</div>
</section>
@ -28,9 +29,9 @@ class JoinedOrganizationList extends Component {
}
}
const mapStateToProps = (state) => ({
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
organizations: state.organizations
organizations: state.organizations,
})
export default connect(
mapStateToProps,

@ -4,25 +4,26 @@ import { contextTypes, childContextTypes, getChildContext, CreateButton, Organiz
import _ from 'lodash'
import moment from 'moment'
import './Organization.css'
import { RootState } from 'actions/types'
// 我拥有和加入的团队
class JoinedOrganizationList extends Component {
class JoinedOrganizationList extends Component<any, any> {
static contextTypes = contextTypes
static childContextTypes = childContextTypes
getChildContext = getChildContext
render () {
let { location, match, organizations } = this.props
render() {
const { location, match, organizations } = this.props
return (
<section className='OrganizationListWrapper'>
<nav className='toolbar clearfix'>
<section className="OrganizationListWrapper">
<nav className="toolbar clearfix">
<OrganizationsTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
<CreateButton />
</nav>
<div className='body'>
<div className="body">
<OrganizationListWithSpin name={location.params.name} organizations={organizations} />
</div>
<div className='footer'>
<div className="footer">
{/* 没有分页! */}
</div>
</section>
@ -30,7 +31,7 @@ class JoinedOrganizationList extends Component {
}
}
const mapStateToProps = (state) => ({
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
joiner: state.auth,
organizations: {
@ -38,8 +39,8 @@ const mapStateToProps = (state) => ({
data: _.uniqBy([...state.ownedOrganizations.data, ...state.joinedOrganizations.data], 'id')
.sort((a, b) => {
return moment(b.updatedAt).diff(a.updatedAt)
})
}
}),
},
})
export default connect(
mapStateToProps,

@ -1,98 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Link } from 'react-router-dom'
// import { Random } from 'mockjs'
import { replaceLocation, handleDelete, handleJoin, handleExit } from './OrganizationListParts'
import { Popover, RModal } from '../utils'
import OrganizationForm from './OrganizationForm'
import { GoOrganization } from 'react-icons/lib/go'
//
class Organization extends Component {
static contextTypes = {
store: PropTypes.object,
onAddOrganization: PropTypes.func,
onDeleteOrganization: PropTypes.func,
onUpdateOrganization: PropTypes.func,
auth: PropTypes.object
}
replaceLocation = replaceLocation
handleUpdate = replaceLocation
handleDelete = handleDelete
handleJoin = handleJoin
// DONE 2.2 退
handleExit = handleExit
constructor (props) {
super(props)
this.state = { update: false }
}
static avatar (user) {
// return Random.dataImage('30x30', user.fullname[0].toUpperCase())
// DONE 2.1 ProductHunt
// DONE 2.1 BOSS
// process.env.NODE_ENV === 'development' ? ... : ...
return `https://work.alibaba-inc.com/photo/${user.empId}.220x220.jpg`
}
render () {
let { auth } = this.context
let { organization } = this.props
let owned = organization.owner.id === auth.id
let joined = organization.members.find(user => user.id === auth.id)
let selfHelpJoin = false // DONE 2.1
return (
<section className='Organization card'>
<div className='card-block'>
<div className='header clearfix'>
<span className='title'>
<GoOrganization className='mr6 color-9' />
<Link to={`/organization/repository?organization=${organization.id}`} >{organization.name}</Link>
</span>
<span className='toolbar'>
{owned || joined ? ( //
<span className='fake-link operation mr5' onClick={e => this.setState({ update: true })}>编辑</span>
) : null}
{this.state.update && (
<RModal when={this.state.update} onClose={e => this.setState({ update: false })} onResolve={e => this.handleUpdate()}>
<OrganizationForm title={`编辑团队 #${organization.id}`} organization={organization} />
</RModal>
)}
{owned ? ( //
<Link to='' onClick={e => this.handleDelete(e, organization)} className='operation mr5'>删除</Link>
) : null}
{!owned && joined ? ( //
<Link to='' onClick={e => this.handleExit(e, organization)} className='operation mr5'>退出</Link>
) : null}
{!owned && !joined && selfHelpJoin ? ( //
<Link to='' onClick={e => this.handleJoin(e, organization)} className='operation mr5'>加入</Link>
) : null}
</span>
</div>
<div className='body'>
<div className='desc'>{organization.description}</div>
<div className='members'>
<Popover content={`${organization.owner.fullname} ${organization.owner.id}`}>
<img alt={organization.owner.fullname} src={Organization.avatar(organization.owner)} className='avatar owner' />
</Popover>
{organization.members.map(user =>
<Popover key={user.id} content={`${user.fullname} ${user.id}`}>
<img alt={user.fullname} title={user.fullname} src={Organization.avatar(user)} className='avatar' />
</Popover>
)}
</div>
</div>
</div>
</section>
)
}
}
//
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Organization)

@ -0,0 +1,67 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { Popover } from '../utils'
import OrganizationForm from './OrganizationForm'
import { GoOrganization } from 'react-icons/go'
import { useSelector } from 'react-redux'
import { RootState, Organization } from '../../actions/types'
import { handleDelete, handleExit, handleJoin } from './OrganizationListParts'
function avatar(user: any) {
return `https://work.alibaba-inc.com/photo/${user.empId}.220x220.jpg`
}
interface Props {
organization: Organization
}
function OrganizationBlock(props: Props) {
const { organization } = props
const auth = useSelector((state: RootState) => state.auth)
const [update, setUpdate] = useState(false)
const owned = organization.owner && organization.owner.id === auth.id
const joined = organization.members!.find(user => user.id === auth.id)
const selfHelpJoin = false // DONE 2.1 不允许自助加入团队
return (
<section className="Organization card">
<div className="card-block">
<div className="header clearfix">
<span className="title">
<GoOrganization className="mr6 color-9" />
<Link to={`/organization/repository?organization=${organization.id}`} >{organization.name}</Link>
</span>
<span className="toolbar">
{owned || joined ? ( // 拥有或已加入
<span className="fake-link operation mr5" onClick={() => setUpdate(true)}></span>
) : null}
<OrganizationForm organization={organization} open={update} onClose={() => setUpdate(false)} />
{owned ? ( // 拥有
<Link to="" onClick={e => handleDelete(e, organization)} className="operation mr5"></Link>
) : null}
{!owned && joined ? ( // 不拥有,已加入
<Link to="" onClick={e => handleExit(e, organization)} className="operation mr5">退</Link>
) : null}
{!owned && !joined && selfHelpJoin ? ( // 不拥有,未加入
<Link to="" onClick={e => handleJoin(e, organization)} className="operation mr5"></Link>
) : null}
</span>
</div>
<div className="body">
<div className="desc">{organization.description}</div>
<div className="members">
<Popover content={`${organization.owner!.fullname} ${organization.owner!.id}`}>
<img alt={organization.owner!.fullname} src={avatar(organization.owner)} className="avatar owner" />
</Popover>
{organization.members!.map(user =>
<Popover key={user.id} content={`${user.fullname} ${user.id}`}>
<img alt={user.fullname} title={user.fullname} src={avatar(user)} className="avatar" />
</Popover>
)}
</div>
</div>
</div>
</section>
)
}
export default OrganizationBlock

@ -1,141 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import { SmartTextarea, MembersInput, RParsley } from '../utils'
import RadioList from '../utils/RadioList'
import { FORM } from '../../family/UIConst'
//
const mockOrganization = process.env.NODE_ENV === 'development'
? () => Mock.mock({
name: '团队@CTITLE(5)',
description: '@CPARAGRAPH',
logo: '@URL',
ownerId: undefined,
members: []
})
: () => ({
name: '',
description: '',
logo: '',
ownerId: undefined,
members: []
})
//
// TODO 2.x
// TODO 2.x
class OrganizationForm extends Component {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddOrganization: PropTypes.func.isRequired,
onUpdateOrganization: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
organization: PropTypes.object
}
constructor (props) {
super(props)
let { organization } = props
this.state = organization ? {
...organization,
// members: organization.members.map(user => user.id).join(',')
members: organization.members.filter(user => !!user),
newOwner: organization.owner
} : mockOrganization()
}
render () {
const { rmodal } = this.context
let { auth } = this.props
return (
<section>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<RParsley ref={rparsley => { this.rparsley = rparsley }}>
<form className='form-horizontal w600' onSubmit={this.handleSubmit} >
<div className='rmodal-body'>
{this.state.id &&
<div className='form-group row'>
<label className='col-sm-2 control-label'>拥有者</label>
<div className='col-sm-10'>
{this.state.owner && (this.state.owner.id === auth.id)
? <MembersInput value={this.state.newOwner ? [this.state.newOwner] : []} limit={1} onChange={users => this.setState({ newOwner: users[0] })} />
: <div className='pt7 pl9'>{this.state.owner.fullname}</div>
}
</div>
</div>
}
<div className='form-group row'>
<label className='col-sm-2 control-label'>权限</label>
<div className='col-sm-10'>
<RadioList data={FORM.RADIO_LIST_DATA_VISIBILITY} curVal={this.state.visibility} name='visibility'
onChange={visibility => this.setState({ visibility })} />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>名称</label>
<div className='col-sm-10'>
<input name='name' value={this.state.name} onChange={e => this.setState({ name: e.target.value })} className='form-control' placeholder='Name' spellCheck='false' autoFocus='true'
required data-parsley-trigger='change keyup' data-parsley-maxlength='256' />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>简介</label>
<div className='col-sm-10'>
<SmartTextarea name='description' value={this.state.description} onChange={e => this.setState({ description: e.target.value })} className='form-control' placeholder='Description' spellCheck='false' rows='5'
data-parsley-trigger='change keyup' data-parsley-maxlength='1024' />
</div>
</div>
<div className='form-group row'>
<label className='col-sm-2 control-label'>成员</label>
<div className='col-sm-10'>
<MembersInput value={this.state.members} onChange={members => this.setState({ members })} />
</div>
</div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button type='submit' className='btn btn-success w140 mr20'>提交</button>
<Link to='' onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10'>取消</Link>
</div>
</div>
</div>
</form>
</RParsley>
</section>
)
}
handleSubmit = (e) => {
e.preventDefault()
let { onAddOrganization, onUpdateOrganization } = this.context
let onAddOrUpdateOrganization = this.state.id ? onUpdateOrganization : onAddOrganization
let organization = {
...this.state,
// owner: auth.id, //
memberIds: (this.state.members || []).map(user => user.id)
}
let { owner, newOwner } = this.state
if (newOwner && newOwner.id !== owner.id) organization.ownerId = newOwner.id
onAddOrUpdateOrganization(organization, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
// creator ownerX constructor creator owner
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrganizationForm)

@ -0,0 +1,177 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import RadioList from '../utils/RadioList'
import { FORM, YUP_MSG } from '../../family/UIConst'
import { Formik, Field, Form } from 'formik'
import { TextField } from 'formik-material-ui'
import * as Yup from 'yup'
import { Button, Theme, Dialog, Slide, DialogContent, DialogTitle } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { Organization, RootState } from '../../actions/types'
import UserList from '../common/UserList'
import AccountService from '../../relatives/services/Account'
import * as _ from 'lodash'
import { updateOrganization, addOrganization } from '../../actions/organization'
import { refresh } from '../../actions/common'
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
minHeight: 300,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
}))
const schema = Yup.object().shape<Partial<Organization>>({
name: Yup.string().required(YUP_MSG.REQUIRED).max(20, YUP_MSG.MAX_LENGTH(20)),
description: Yup.string().max(1000, YUP_MSG.MAX_LENGTH(1000)),
})
const FORM_STATE_INIT: Organization = {
id: 0,
name: '',
description: '',
members: [],
visibility: false,
}
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />
})
interface Props {
open: boolean
onClose: (isOk?: boolean) => void
organization?: Organization
}
function OrganizationForm(props: Props) {
const { open, onClose, organization } = props
const auth = useSelector((state: RootState) => state.auth)
const classes = useStyles()
const dispatch = useDispatch()
return (
<Dialog
open={open}
onClose={() => onClose()}
TransitionComponent={Transition}
>
<DialogTitle></DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
...(organization || {}),
}}
validationSchema={schema}
onSubmit={(values) => {
const addOrUpdateOrganization = values.id ? updateOrganization : addOrganization
const organization: Organization = {
...values,
memberIds: (values.members || []).map(user => user.id),
}
const { owner, newOwner } = values
if (newOwner && newOwner.id !== owner!.id) { organization.ownerId = newOwner.id }
dispatch(addOrUpdateOrganization(organization, () => {
dispatch(refresh())
onClose(true)
}))
}}
render={({ isSubmitting, setFieldValue, values }) => {
function loadUserOptions(input: string): Promise<Array<{ label: string, value: number }>> {
return new Promise(async (resolve) => {
const users = await AccountService.fetchUserList({ name: input })
const options = _.differenceWith(users.data, values.members || [], _.isEqual)
resolve(options.map(x => ({ label: `${x.fullname} ${x.empId || x.email}`, value: x.id })))
})
}
return (
<Form>
<div className="rmodal-body">
{values.id > 0 &&
<div className={classes.formItem}>
<div className={classes.formTitle}>Owner</div>
{values.owner && (values.owner.id === auth.id)
? <UserList
isMulti={false}
value={values.newOwner ? [{ label: values.newOwner.fullname, value: values.newOwner.id }] : []}
loadOptions={loadUserOptions}
onChange={(users: any) => setFieldValue('newOwner', users[0])}
/>
: <div className="pt7 pl9">{values.owner!.fullname}</div>
}
</div>
}
<div className={classes.formItem}>
<RadioList
data={FORM.RADIO_LIST_DATA_VISIBILITY}
curVal={values.visibility}
name="visibility"
onChange={(val: any) => setFieldValue('visibility', val)}
/>
</div>
<div className={classes.formItem}>
<Field
name="name"
label="团队名称"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<Field
name="description"
label="描述"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<div className={classes.formTitle}></div>
<UserList
isMulti={true}
loadOptions={loadUserOptions}
selected={values.members!.map(x => ({ label: x.fullname, value: x.id }))}
onChange={selected => setFieldValue('members', selected)}
/>
</div>
</div>
<div className={classes.ctl}>
<Button type="submit" variant="contained" color="primary" className="mr1" disabled={isSubmitting}></Button>
<Button onClick={() => onClose()} disabled={isSubmitting}></Button>
</div>
</Form>
)
}}
/>
</div>
</DialogContent>
</Dialog>
)
}
export default OrganizationForm

@ -1,35 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import Organization from './Organization'
//
class OrganizationList extends Component {
// DONE 2.1 propTypes
static propTypes = {
name: PropTypes.string,
organizations: PropTypes.array.isRequired
}
render () {
let { name, organizations } = this.props
if (!organizations.length) {
return name
? <div className='fontsize-14 text-center p40'>没有找到匹配 <strong>{name}</strong> 的团队</div>
: <div className='fontsize-14 text-center p40'>没有数据</div>
}
return (
<div className='OrganizationList'>
{organizations.map(organization =>
<Organization key={organization.id} organization={organization} />
)}
</div>
)
}
}
//
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrganizationList)

@ -0,0 +1,29 @@
import React, { Component } from 'react'
import { PropTypes } from '../../family'
import Organization from './Organization'
// 展示组件
class OrganizationList extends Component<any, any> {
// DONE 2.1 补全 propTypes
static propTypes = {
name: PropTypes.string,
organizations: PropTypes.array.isRequired,
}
render() {
const { name, organizations } = this.props
if (!organizations.length) {
return name
? <div className="fontsize-14 text-center p40"> <strong>{name}</strong> </div>
: <div className="fontsize-14 text-center p40"></div>
}
return (
<div className="OrganizationList">
{organizations.map((organization: any) =>
<Organization key={organization.id} organization={organization} />
)}
</div>
)
}
}
export default OrganizationList

@ -1,179 +0,0 @@
import React, { Component } from 'react'
import { PropTypes, push, replace, URI, StoreStateRouterLocationURI } from '../../family'
import { Spin, RModal, Pagination } from '../utils'
import OrganizationList from './OrganizationList'
import OrganizationForm from './OrganizationForm'
import { addOrganization, deleteOrganization, updateOrganization } from '../../actions/organization'
import { GoOrganization } from 'react-icons/lib/go'
export const contextTypes = {
store: PropTypes.object
}
export const childContextTypes = {
history: PropTypes.object,
location: PropTypes.object,
match: PropTypes.object,
onAddOrganization: PropTypes.func,
onDeleteOrganization: PropTypes.func,
onUpdateOrganization: PropTypes.func,
auth: PropTypes.object
}
export function getChildContext () {
let { history, location, match, onAddOrganization, onDeleteOrganization, onUpdateOrganization, auth } = this.props
return { history, location, match, onAddOrganization, onDeleteOrganization, onUpdateOrganization, auth }
}
export const mapDispatchToProps = ({
onAddOrganization: addOrganization,
onDeleteOrganization: deleteOrganization,
onUpdateOrganization: updateOrganization
})
export class CreateButton extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = { create: false }
}
render () {
return (
<span className='float-right ml10'>
<button className='OrganizationCreateButton btn btn-success' onClick={e => this.setState({ create: true })}>
<GoOrganization /> 新建团队
</button>
{this.state.create && (
<RModal when={this.state.create} onClose={e => this.setState({ create: false })} onResolve={this.handleUpdate}>
<OrganizationForm title='新建团队' />
</RModal>
)}
</span>
)
}
handleUpdate = (e) => {
}
}
// TODO 2.2 <select> => <Dropdown>
export class OrganizationsTypeDropdown extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
render () {
let { url } = this.props
return (
<select className='OrganizationsTypeDropdown form-control float-left w160 mr12' value={url} onChange={e => this.handlePush(e.target.value)} size='1'>
<option value='/organization/joined'>我拥有和加入的团队</option>
<option value='/organization/all'>全部团队</option>
</select>
)
}
handlePush = (url) => {
let { store } = this.context
store.dispatch(push(url))
}
}
export class SearchGroup extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = { name: props.name || '' }
}
render () {
// <div className='input-group float-right w280'>
// <input type='text' value={this.state.name} className='form-control' placeholder=' ID' autoComplete='off'
// onChange={e => this.setState({ name: e.target.value.trim() })}
// onKeyUp={e => e.which === 13 && this.handleSearch()} />
// <span className='btn input-group-addon' onClick={this.handleSearch}><span className=''>&#xe60b;</span></span>
// </div>
return (
<input type='text' value={this.state.name} className='form-control float-left w280' placeholder='搜索团队:输入名称或 ID' autoComplete='off'
onChange={e => this.setState({ name: e.target.value.trim() })}
onKeyUp={e => e.which === 13 && this.handleSearch()}
ref={$input => { this.$input = $input }} />
)
}
componentDidMount () {
if (this.state.name) {
this.$input.focus()
}
}
handleSearch = () => {
let { store } = this.context
let { pathname, hash, search } = store.getState().router.location
let uri = URI(pathname + hash + search).removeSearch('cursor')
this.state.name ? uri.setSearch('name', this.state.name) : uri.removeSearch('name')
store.dispatch(push(uri.href()))
}
}
// DONE Dialog Component
// DONE 2.1 replaceLocation
// DONE 2.1
export function handleDelete (e, organization) {
e.preventDefault()
let message = `团队被删除后不可恢复!\n确认继续删除『#${organization.id} ${organization.name}』吗?`
if (!window.confirm(message)) return
let { onDeleteOrganization } = this.context
onDeleteOrganization(organization.id, () => {
this.replaceLocation()
})
}
// DONE
export function replaceLocation () {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
export function handleJoin (e, organization) {
e.preventDefault()
let { auth, onUpdateOrganization } = this.context
let next = {
id: organization.id,
memberIds: [...organization.members.map(user => user.id), auth.id]
}
onUpdateOrganization(next, () => {
this.replaceLocation()
})
}
export function handleExit (e, organization) {
e.preventDefault()
let message = `确认继续退出『#${organization.id} ${organization.name}』吗?`
if (!window.confirm(message)) return
let { auth, onUpdateOrganization } = this.context
let next = {
id: organization.id,
memberIds: organization.members.filter(user => user.id !== auth.id).map(user => user.id)
}
onUpdateOrganization(next, () => {
this.replaceLocation()
})
}
export const OrganizationListWithSpin = ({ name, organizations }) => (
organizations.fetching
? <Spin />
: <OrganizationList name={name} organizations={organizations.data} />
)
export class PaginationWithLocation extends Component {
static contextTypes = {
location: PropTypes.object
}
render () {
let { calculated } = this.props
let { location } = this.context
return <Pagination location={location} calculated={calculated} />
}
}

@ -0,0 +1,152 @@
import React, { Component, useState, useEffect } from 'react'
import { PropTypes, push, replace, URI, StoreStateRouterLocationURI } from '../../family'
import { Spin, Pagination } from '../utils'
import OrganizationList from './OrganizationList'
import OrganizationForm from './OrganizationForm'
import { addOrganization, deleteOrganization, updateOrganization } from '../../actions/organization'
import { useDispatch, useSelector } from 'react-redux'
import { Select, MenuItem, TextField, Button } from '@material-ui/core'
import { RootState } from 'actions/types'
export const contextTypes = {
store: PropTypes.object,
}
export const childContextTypes = {
history: PropTypes.object,
location: PropTypes.object,
match: PropTypes.object,
onAddOrganization: PropTypes.func,
onDeleteOrganization: PropTypes.func,
onUpdateOrganization: PropTypes.func,
auth: PropTypes.object,
}
export function getChildContext(this: any) {
const { history, location, match, onAddOrganization, onDeleteOrganization, onUpdateOrganization, auth } = this.props
return { history, location, match, onAddOrganization, onDeleteOrganization, onUpdateOrganization, auth }
}
export const mapDispatchToProps = ({
onAddOrganization: addOrganization,
onDeleteOrganization: deleteOrganization,
onUpdateOrganization: updateOrganization,
})
export function CreateButton() {
const [open, setOpen] = useState(false)
return (
<span className="float-right ml10">
<Button className="OrganizationCreateButton" variant="contained" color="primary" onClick={() => setOpen(true)}> </Button>
<OrganizationForm open={open} onClose={() => setOpen(false)} />
</span>
)
}
// TODO 2.2 <select> => <Dropdown>
export function OrganizationsTypeDropdown({ url }: { url: string }) {
const dispatch = useDispatch()
const handlePush = (url: string) => {
dispatch(push(url))
}
return (
<Select
className="mr8"
value={url}
onChange={e => handlePush(e.target.value as string)}
>
<MenuItem value="/organization/joined"></MenuItem>
<MenuItem value="/organization/all"></MenuItem>
</Select>
)
}
export function SearchGroup(props: { name: string }) {
const { name } = props
const dispatch = useDispatch()
const router = useSelector((state: RootState) => state.router)
const [query, setQuery] = useState('')
const handleSearch = () => {
const { pathname, hash, search } = router.location
const uri = URI(pathname + hash + search).removeSearch('cursor')
query ? uri.setSearch('name', query) : uri.removeSearch('name')
dispatch(push(uri.href()))
}
useEffect(() => {
setQuery(name)
}, [name])
return (
<TextField
value={query || ''}
placeholder="搜索仓库名称、ID"
autoComplete="off"
onChange={e => setQuery(e.target.value.trim())}
onKeyUp={e => e.which === 13 && handleSearch()}
style={{ width: 200 }}
/>
)
}
// DONE 把控制钱从 Dialog 移动到 Component 中!
// DONE 2.1 通常应该用 replaceLocation
// DONE 2.1 删除确认
export function handleDelete(this: any, e: any, organization: any) {
e.preventDefault()
const message = `团队被删除后不可恢复!\n确认继续删除『#${organization.id} ${organization.name}』吗?`
if (!window.confirm(message)) { return }
const { onDeleteOrganization } = this.context
onDeleteOrganization(organization.id, () => {
this.replaceLocation()
})
}
// DONE 重构表之间的对应关系,分为多张表。否则每次更新成员都会导致当前团队排到第一位!待测试。
export function replaceLocation(this: any) {
const { router } = this.props
const uri = StoreStateRouterLocationURI(router)
replace(uri.href())
}
export function handleJoin(this: any, e: any, organization: any) {
e.preventDefault()
const { auth, onUpdateOrganization } = this.context
const next = {
id: organization.id,
memberIds: [...organization.members.map((user: any) => user.id), auth.id],
}
onUpdateOrganization(next, () => {
this.replaceLocation()
})
}
export function handleExit(this: any, e: any, organization: any) {
e.preventDefault()
const message = `确认继续退出『#${organization.id} ${organization.name}』吗?`
if (!window.confirm(message)) { return }
const { auth, onUpdateOrganization } = this.context
const next = {
id: organization.id,
memberIds: organization.members.filter((user: any) => user.id !== auth.id).map((user: any) => user.id),
}
onUpdateOrganization(next, () => {
this.replaceLocation()
})
}
export const OrganizationListWithSpin = ({ name, organizations }: any) => (
organizations.fetching
? <Spin />
: <OrganizationList name={name} organizations={organizations.data} />
)
export class PaginationWithLocation extends Component<any, any> {
static contextTypes = {
location: PropTypes.object,
}
render() {
const { calculated } = this.props
const { location } = this.context
return <Pagination location={location} calculated={calculated} />
}
}

@ -1,49 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { contextTypes, childContextTypes, getChildContext, CreateButton, SearchGroup, mapDispatchToProps, OrganizationRepositoryListWithSpin, PaginationWithLocation } from '../repository/RepositoryListParts'
import { Spin } from '../utils'
import '../repository/Repository.css'
//
class OrganizationRepositoryList extends Component {
static contextTypes = contextTypes
static propTypes = {}
static childContextTypes = childContextTypes
getChildContext = getChildContext.bind(this)
render () {
let { location, auth, organization, repositories } = this.props
if (!organization.id) return <Spin />
let isOwned = organization.owner.id === auth.id
let isJoined = organization.members.find(itme => itme.id === auth.id)
return (
<section className='RepositoryListWrapper'>
<div className='header'><span className='title'>{organization.name}</span></div>
<nav className='toolbar clearfix'>
{isOwned || isJoined
? <CreateButton organization={organization} />
: null
}
<SearchGroup name={location.params.name} />
</nav>
<div className='body'>
<OrganizationRepositoryListWithSpin name={location.params.name} repositories={repositories} />
</div>
<div className='footer'>
<PaginationWithLocation calculated={repositories.pagination} />
</div>
</section>
)
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth,
organization: state.organization,
repositories: state.repositories // TODO => organizationRepositories
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrganizationRepositoryList)

@ -0,0 +1,46 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { CreateButton, SearchGroup, mapDispatchToProps, OrganizationRepositoryListWithSpin, PaginationWithLocation } from '../repository/RepositoryListParts'
import { Spin } from '../utils'
import '../repository/Repository.css'
import { RootState } from 'actions/types'
// 展示组件
class OrganizationRepositoryList extends Component<any, any> {
render() {
const { location, auth, organization, repositories } = this.props
if (!organization || !organization.id) { return <Spin /> }
const isOwned = organization.owner.id === auth.id
const isJoined = organization.members.find((item: any) => item.id === auth.id)
return (
<section className="RepositoryListWrapper">
<div className="header"><span className="title">{organization.name}</span></div>
<nav className="toolbar clearfix">
{isOwned || isJoined
? <CreateButton organization={organization} />
: null
}
<SearchGroup name={location.params.name} />
</nav>
<div className="body">
<OrganizationRepositoryListWithSpin name={location.params.name} repositories={repositories} />
</div>
<div className="footer">
<PaginationWithLocation calculated={repositories.pagination} />
</div>
</section>
)
}
}
// 容器组件
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
organization: state.organization,
repositories: state.repositories, // TODO => organizationRepositories
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(OrganizationRepositoryList)

@ -1,38 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { contextTypes, childContextTypes, getChildContext, RepositoriesTypeDropdown, SearchGroup, mapDispatchToProps, RepositoryListWithSpin, PaginationWithLocation } from './RepositoryListParts'
import './Repository.css'
//
class AllRepositoryList extends Component {
static contextTypes = contextTypes
static propTypes = {}
static childContextTypes = childContextTypes
getChildContext = getChildContext.bind(this)
render () {
let { location, match, repositories } = this.props
return (
<section className='RepositoryListWrapper'>
<nav className='toolbar clearfix'>
<RepositoriesTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
</nav>
<div className='body'>
<RepositoryListWithSpin name={location.params.name} repositories={repositories} />
</div>
<div className='footer'>
<PaginationWithLocation calculated={repositories.pagination} />
</div>
</section>
)
}
}
const mapStateToProps = (state) => ({
joiner: state.auth,
repositories: state.repositories
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(AllRepositoryList)

@ -0,0 +1,35 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { RepositoriesTypeDropdown, SearchGroup, mapDispatchToProps, RepositoryListWithSpin, PaginationWithLocation } from './RepositoryListParts'
import './Repository.css'
import { RootState } from 'actions/types'
// 全部仓库
class AllRepositoryList extends Component<any, any> {
render() {
const { location, match, repositories } = this.props
return (
<section className="RepositoryListWrapper">
<nav className="toolbar clearfix">
<RepositoriesTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
</nav>
<div className="body">
<RepositoryListWithSpin name={location.params.name} repositories={repositories} />
</div>
<div className="footer">
<PaginationWithLocation calculated={repositories.pagination} />
</div>
</section>
)
}
}
const mapStateToProps = (state: RootState) => ({
joiner: state.auth,
repositories: state.repositories,
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(AllRepositoryList)

@ -1,69 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { RParsley } from '../utils'
import config from '../../config'
class ExportPostmanForm extends Component {
static contextTypes = {
rmodal: PropTypes.object.isRequired
}
static propTypes = {
repoId: PropTypes.number
}
render () {
const { rmodal } = this.context
const { repoId } = this.props
return (
<section className='RepositoryForm'>
<div className='rmodal-header'>
<span className='rmodal-title'>{this.props.title}</span>
</div>
<RParsley ref={rparsley => { this.rparsley = rparsley }}>
<form className='form-horizontal' onSubmit={() => false} >
<div className='rmodal-body'>
<div>
请在Postman中点击导入Import选择从链接导入Import From Link输入如下链接即可
</div>
<div className='alert alert-info' role='alert' style={{ margin: '8px 0' }}> {config.serve}/postman/export?id={repoId} </div>
</div>
<div className='rmodal-footer'>
<div className='form-group row mb0'>
<label className='col-sm-2 control-label' />
<div className='col-sm-10'>
<button onClick={e => { e.preventDefault(); rmodal.close() }} className='mr10 btn btn-secondary'>关闭</button>
</div>
</div>
</div>
</form>
</RParsley>
</section>
)
}
componentDidUpdate () {
this.context.rmodal.reposition()
}
handleSubmit = (e) => {
e.preventDefault()
if (!this.rparsley.isValid()) return
let { onAddRepository, onUpdateRepository } = this.context
let onAddOrUpdateRepository = this.state.id ? onUpdateRepository : onAddRepository
let { organization } = this.props
let repository = {
...this.state,
// ownerId: owner.id, // DONE 2.2
organizationId: organization ? organization.id : null,
memberIds: (this.state.members || []).map(user => user.id)
}
let { owner, newOwner } = this.state
if (newOwner && newOwner.id !== owner.id) repository.ownerId = newOwner.id
let { rmodal } = this.context
rmodal.close()
onAddOrUpdateRepository(repository, () => {
if (rmodal) rmodal.resolve()
})
}
}
export default ExportPostmanForm

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save