first submit

pull/26/merge
Bosn 7 years ago
commit 2d1c7dd278

23
.gitignore vendored

@ -0,0 +1,23 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# css
src/**/*.css
# compile files
build

@ -0,0 +1,77 @@
{
// JSHint Default Configuration File (as on JSHint website)
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : false, // true: Identifiers must be in camelCase
"curly" : false, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
"immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"latedef" : false, // true: Require variables/functions to be defined before being used
"newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` and `--`
"quotmark" : false, // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // Unused variables:
// true : all variables, last function parameter
// "vars" : all variables only
// "strict" : all variables, all function parameters
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : false, // {int} Max number of characters per line
"varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed.
// Relaxing
"asi" : true, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"esversion" : 6, // {int} Specify the ECMAScript version to which the code must adhere.
"moz" : true, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
"esnext" : true,
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : true, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"noyield" : false, // true: Tolerate generator functions with no yield statement in them.
"notypeof" : false, // true: Tolerate invalid typeof operator values
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"devel" : true, // Development/debugging (alert, confirm, etc)
"jquery" : true, // jQuery
"node" : true, // Node.js
"browser" : true, // Browser
// Custom Globals
"globals" : {} // additional predefined global variables
}

@ -0,0 +1,21 @@
language: node_js
cache:
directories:
- node_modules
- $HOME/.npm
notifications:
email: false
t
node_js:
- '8'
before_install:
- npm i -g npm@^5.5.1
script:
- npm run build
- npm run test
after_success:

@ -0,0 +1,13 @@
# rap2-dolores
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
RPA2 前端。
http://rap2.taobao.org/
## 本地开发
```
npm run dev
```

@ -0,0 +1,29 @@
process.env.NODE_ENV = 'production'
// https://nodejs.org/api/cluster.html
// https://github.com/node-modules/graceful/blob/master/example/express_with_cluster/dispatch.js
// http://gitlab.alibaba-inc.com/mm/fb/blob/master/dispatch.js
let cluster = require('cluster')
let path = require('path')
let now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '')
cluster.setupMaster({
exec: path.join(__dirname, 'scripts/worker.js')
})
if (cluster.isMaster) {
require('os').cpus().forEach((cpu, index) => {
cluster.fork()
})
cluster.on('listening', (worker, address) => {
console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} is now connected to ${address.address}:${address.port}.`)
})
cluster.on('disconnect', (worker) => {
console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} has disconnected.`)
})
cluster.on('exit', (worker, code, signal) => {
console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} died (${signal || code}). restarting...`)
cluster.fork()
})
}

@ -0,0 +1,75 @@
{
"name": "rap2-dolores",
"version": "1.0.0",
"description": "",
"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",
"start-js": "react-scripts start",
"start": "npm-run-all -p watch-css start-js",
"build": "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"
},
"repository": {
"type": "git",
"url": "git@gitlab.alibaba-inc.com:thx/rap2-dolores.git"
},
"author": "mozhi.gyy@alibaba-inc.com",
"license": "ISC",
"dependencies": {
"animate.css": "3.5.2",
"bootstrap": "^4.0.0-beta.2",
"chart.js": "^2.6.0",
"codemirror": "5.27.2",
"graceful": "1.0.1",
"koa": "2.3.0",
"koa-router": "7.2.1",
"koa-session": "5.3.0",
"koa-static": "3.0.0",
"lodash": "4.17.4",
"mockjs": "1.0.1-beta3",
"moment": "2.18.1",
"node-fetch": "1.7.1",
"nprogress": "0.2.0",
"parsleyjs": "^2.7.2",
"prop-types": "15.5.10",
"react": "15.6.1",
"react-dom": "15.6.1",
"react-icons": "2.2.5",
"react-modal": "2.1.0",
"react-redux": "5.0.5",
"react-router": "4.1.1",
"react-router-config": "1.0.0-beta.3",
"react-router-dom": "4.1.1",
"react-router-redux": "5.0.0-alpha.6",
"redux": "3.7.1",
"redux-saga": "0.15.4",
"sortablejs": "1.6.0",
"urijs": "1.18.10"
},
"standard": {
"parser": "babel-eslint",
"globals": [
"fetch"
],
"ignore": [
"/build"
]
},
"engines": {
"install-node": "9.2.0"
},
"devDependencies": {
"babel-eslint": "7.2.3",
"node-sass": "4.5.3",
"npm-run-all": "4.0.2",
"pre-commit": "1.2.2",
"react-scripts": "0.9.5",
"standard": "10.0.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>RAP2 Delores</title>
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>

@ -0,0 +1,6 @@
# 1. 你可以直接编辑本文件的内容或者通过工具来帮你校验合法性和自动生成请点击http://aliwing.alibaba-inc.com/apprelease/home.htm
# 2. 更多关于Release文件的规范和约定请点击: http://docs.alibaba-inc.com/pages/viewpage.action?pageId=252891532
# 构建源码语言类型
code.language=nodejs

@ -0,0 +1,69 @@
const fs = require('fs')
const Koa = require('koa')
const serve = require('koa-static')
const Router = require('koa-router')
const session = require('koa-session')
const config = require('../src/config')
const app = new Koa()
app.keys = config.keys
app.use(session(config.session, app))
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
app.use(async (ctx, next) => {
await next()
if (ctx.response.body && ctx.response.body.url) {
ctx.response.body = JSON.stringify(ctx.response.body, null, 4)
}
})
app.use(serve('build'))
let router = new Router()
router.get('/check.node', (ctx) => {
ctx.body = 'success'
})
router.get('/status.taobao', (ctx) => {
ctx.body = 'success'
})
router.get('/test/test.status', (ctx) => {
ctx.body = 'success'
})
router.get('/env', (ctx, next) => {
ctx.body = process.env.NODE_ENV
})
router.get('/delos', (ctx, next) => {
ctx.body = process.env.RAP2_DELOS
})
router.get('/account/info', (ctx) => {
ctx.body = {
url: ctx.request.url,
data: ctx.session.id ? {
id: ctx.session.id,
empId: ctx.session.empId,
fullname: ctx.session.fullname,
email: ctx.session.email
} : undefined
}
})
router.get('/*', (ctx) => {
ctx.type = 'html'
ctx.body = fs.createReadStream('build/index.html')
})
app.use(router.routes())
module.exports = app

@ -0,0 +1,85 @@
(*
Copyright (c) 2015-present, Facebook, Inc.
All rights reserved.
This source code is licensed under the BSD-style license found in the
-- LICENSE file in the root directory of this source tree. An additional grant
of patent rights can be found in the PATENTS file in the same directory.
*)
property targetTab: null
property targetTabIndex: -1
property targetWindow: null
on run argv
set theURL to item 1 of argv
tell application "Chrome"
if (count every window) = 0 then
make new window
end if
-- 1: Looking for tab running debugger
-- then, Reload debugging tab if found
-- then return
set found to my lookupTabWithUrl(theURL)
if found then
set targetWindow's active tab index to targetTabIndex
tell targetTab to reload
tell targetWindow to activate
set index of targetWindow to 1
return
end if
-- 2: Looking for Empty tab
-- In case debugging tab was not found
-- We try to find an empty tab instead
set found to my lookupTabWithUrl("chrome://newtab/")
if found then
set targetWindow's active tab index to targetTabIndex
set URL of targetTab to theURL
tell targetWindow to activate
return
end if
-- 3: Create new tab
-- both debugging and empty tab were not found
-- make a new tab with url
tell window 1
activate
make new tab with properties {URL:theURL}
end tell
end tell
end run
-- Function:
-- Lookup tab with given url
-- if found, store tab, index, and window in properties
-- (properties were declared on top of file)
on lookupTabWithUrl(lookupUrl)
tell application "Chrome"
-- Find a tab with the given url
set found to false
set theTabIndex to -1
repeat with theWindow in every window
set theTabIndex to 0
repeat with theTab in every tab of theWindow
set theTabIndex to theTabIndex + 1
if (theTab's URL as string) contains lookupUrl then
-- assign tab, tab index, and window to properties
set targetTab to theTab
set targetTabIndex to theTabIndex
set targetWindow to theWindow
set found to true
exit repeat
end if
end repeat
if found then
exit repeat
end if
end repeat
end tell
return found
end lookupTabWithUrl

@ -0,0 +1,17 @@
// https://github.com/node-modules/graceful
let graceful = require('graceful')
let now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '')
let app = require('./app')
let PORT = 8080
let server = app.listen(PORT, () => {
console.log(`[${now()}] worker#${process.pid} rap2-dolores is running as ${PORT}`)
})
graceful({
servers: [server],
killTimeout: '10s',
error: (err, throwErrorCount) => {
if (err.message) err.message += ` (uncaughtException throw ${throwErrorCount} times on pid:${process.pid})`
console.error(`[${now()}] worker#${process.pid}] ${err.message}`)
}
})

@ -0,0 +1,57 @@
// 登陆
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 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 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,20 @@
// 获取平台计数信息
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,27 @@
export const addInterface = (itf, onResolved) => ({ type: 'INTERFACE_ADD', interface: itf, onResolved })
export const addInterfaceSucceeded = (itf) => ({ type: 'INTERFACE_ADD_SUCCEEDED', interface: itf })
export const addInterfaceFailed = (message) => ({ type: 'INTERFACE_ADD_FAILED', message })
export const updateInterface = (itf, onResolved) => ({ type: 'INTERFACE_UPDATE', interface: itf, onResolved })
export const updateInterfaceSucceeded = (itf) => ({ type: 'INTERFACE_UPDATE_SUCCEEDED', interface: itf })
export const updateInterfaceFailed = (message) => ({ type: 'INTERFACE_UPDATE_FAILED', message })
export const deleteInterface = (id, onResolved) => ({ type: 'INTERFACE_DELETE', id, onResolved })
export const deleteInterfaceSucceeded = (id) => ({ type: 'INTERFACE_DELETE_SUCCEEDED', id })
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 = (id) => ({ type: 'INTERFACE_LOCK_SUCCEEDED', id })
export const lockInterfaceFailed = (message) => ({ type: 'INTERFACE_LOCK_FAILED', message })
export const unlockInterface = (id, onResolved) => ({ type: 'INTERFACE_UNLOCK', id, onResolved })
export const unlockInterfaceSucceeded = (itf) => ({ type: 'INTERFACE_UNLOCK_SUCCEEDED', interface: itf })
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,15 @@
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 = (module) => ({ type: 'MODULE_UPDATE_SUCCEEDED', module })
export const updateModuleFailed = (message) => ({ type: 'MODULE_UPDATE_FAILED', message })
export const deleteModule = (id, onResolved) => ({ type: 'MODULE_DELETE', id, onResolved })
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,31 @@
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,19 @@
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, onResolved) => ({ type: 'PROPERTIES_UPDATE', itf, properties, onResolved })
export const updatePropertiesSucceeded = (properties) => ({ type: 'PROPERTIES_UPDATE_SUCCEEDED', properties })
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,33 @@
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 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,13 @@
## 支持 Sasss
[Adding a CSS Preprocessor (Sass, Less etc.)](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-a-css-preprocessor-sass-less-etc)
## CSS 规约
[集团开发规约 - CSS 规约](http://groups.alidemo.cn/f2e-specs/style-guide/_book/1.coding/2.css-style-guide.html)
[网易前端 - CSS 规范](http://nec.netease.com/standard/css-sort.html)
[ecomfe - CSS编码规范](https://github.com/ecomfe/spec/blob/master/css-style-guide.md)
## 参考
* [常用的CSS命名规则](https://www.zhihu.com/question/19586885)

@ -0,0 +1,152 @@
@import "./variables.sass";
// ----------------------------------------
// Input
// Textarea
// ----------------------------------------
.form-control
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
padding: 6px 9px;
border: 1px solid $border;
border-radius: 4px;
box-shadow: none;
font-size: 12px;
background-color: white;
&:focus
border-color: $brand;
outline: 0;
input.form-control,
select.form-control
height: 32px;
input[type="radio"],
input[type="checkbox"]
margin-top: 3px;
.form-horizontal
label.control-label
padding-top: 7px;
padding-right: 0;
label.radio-inline
padding-top: 7px;
padding-right: 0;
margin: 0 15px 10px 0;
input[type=radio]
margin-right: 10px;
@media (max-width: 575px)
.form-horizontal
label.control-label
text-align: left;
@media (min-width: 576px)
.form-horizontal
label.control-label
text-align: right;
// ----------------------------------------
// Button
// ----------------------------------------
.btn
line-height: 1.5;
.rapfont
line-height: 1rem;
margin-right: .5rem;
a.btn.btn-success
color: white;
// .btn
// font-size: 12px;
// line-height: 1.5;
// padding: 6px 20px;
// border: 1px solid $border;
// border-radius: 4px;
// background-color: $border;
// cursor: pointer;
// // height: 32px;
// &:hover
// background-color: #ccc;
// border-color: $border;
// &:focus
// outline: none;
// box-shadow: none;
// &.btn-primary
// color: white;
// border-color: $brand;
// background-color: $brand;
// &:hover
// background-color: $brand-hover;
// &.btn-rapfont
// padding-right: 2rem;
// .rapfont
// font-size: 1rem;
// margin-right: 0.5rem;
// ----------------------------------------
// Table
// ----------------------------------------
table.table
> thead
> tr
th
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
> tbody
> tr
> td
vertical-align: middle;
.operation
visibility: hidden;
> tr:hover
> td
.operation
visibility: visible;
// ----------------------------------------
// Panel
// ----------------------------------------
// .panel
// margin-bottom: 1rem;
// border: 1px solid $border;
// border-radius: 0.3rem;
// > .panel-header,
// > .panel-footer
// padding: 0.75rem 1.25rem;
// background-color: $bg;
// > .panel-header
// border-bottom: 1px solid $border;
// > .panel-footer
// border-top: 1px solid $border;
// > .panel-body
// padding: 1.25rem;
// ----------------------------------------
//
// ----------------------------------------
.OpeningScreenAdvertising
position: absolute;
left: 50%;
top: 50%;
width: 20rem;
height: 4rem;
margin-left: -10rem;
margin-top: -2rem;
.Spin
margin: 0;
padding: 0;
// card
.card-block
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
padding: 1.25rem;

@ -0,0 +1,25 @@
// ----------------------------------------
//
// ----------------------------------------
@font-face
font-family: 'brixfont'; /* project id 12401 */
src: url('//at.alicdn.com/t/font_p7lto2nib7h6ko6r.eot');
src: url('//at.alicdn.com/t/font_p7lto2nib7h6ko6r.eot?#iefix') format('embedded-opentype'), url('//at.alicdn.com/t/font_p7lto2nib7h6ko6r.woff') format('woff'), url('//at.alicdn.com/t/font_p7lto2nib7h6ko6r.ttf') format('truetype'), url('//at.alicdn.com/t/font_p7lto2nib7h6ko6r.svg#iconfont') format('svg');
.brixfont
font-family: "brixfont" !important;
@font-face
font-family: 'rapfont'; /* project id 182314 */
src: url('//at.alicdn.com/t/font_afztm7sue48wipb9.eot');
src: url('//at.alicdn.com/t/font_afztm7sue48wipb9.eot?#iefix') format('embedded-opentype'), url('//at.alicdn.com/t/font_afztm7sue48wipb9.woff') format('woff'), url('//at.alicdn.com/t/font_afztm7sue48wipb9.ttf') format('truetype'), url('//at.alicdn.com/t/font_afztm7sue48wipb9.svg#rapfont') format('svg');
.rapfont
font-family: "rapfont" !important;
@font-face
font-family: 'techfont';
src: url('//g.alicdn.com/mm/zuanshi/20161214.111214.492/app/lib/Oswald-Bold.ttf');
src: url('//g.alicdn.com/mm/zuanshi/20161214.111214.492/app/lib/Oswald-Bold.ttf?#iefix') format('embedded-opentype'), url('//g.alicdn.com/mm/zuanshi/20161214.111214.492/app/lib/Oswald-Bold.ttf') format('woff'), url('//g.alicdn.com/mm/zuanshi/20161214.111214.492/app/lib/Oswald-Bold.ttf') format('truetype'), url('//g.alicdn.com/mm/zuanshi/20161214.111214.492/app/lib/Oswald-Bold.ttf#FZZDHJW--GB1-0') format('svg');
.techfont
font-family: 'techfont';

@ -0,0 +1,73 @@
// bootstrap v4
// GitHub
// TODO 2.x codesandbox.io
// @import '../../node_modules/bootstrap/dist/css/bootstrap.css';
@import "./fonts.sass"
@import "./variables.sass"
@import "./components.sass"
@import "./shortcuts.sass"
// @import "../../node_modules/animate.css/animate.css"
html
font-size: 62.5%; // 10 ÷ 16 × 100% = 62.5%
body
font-size: 1.2rem;
line-height: 1.5;
font-family: $font-family;
-webkit-font-smoothing: antialiased;
color: #24292e;
html, body
height: 100%;
#root
height: 100%;
> .Routes
display: flex;
flex-direction: column;
min-height: 100%;
> .body
flex-grow: 1;
display: flex;
flex-direction: column;
> .Spin
align-items: center;
justify-content: center;
> * //
flex-grow: 1;
display: flex;
flex-direction: column;
> .body
flex-grow: 1;
button, input, optgroup, select, textarea
font-size: 1.2rem;
font-family: $font-family;
color: #24292e;
-webkit-font-smoothing: antialiased;
// *
// transition: color .15s ease-out, background-color .15s ease-out, opacity .15s ease-out;
a
&, &:hover, &:focus, &:active, &:visited
outline: 0;
text-decoration: none;
&[disabled],
&.disabled
pointer-events: none;
cursor: not-allowed;
a.text-decoration
&:hover
border-bottom: 1px solid $brand;
pre
margin: 0;
padding: 0.5rem 0.75rem;
border: 1px solid $border;
border-radius: 0.4rem;
background-color: $bg;
white-space: pre-wrap;

@ -0,0 +1,103 @@
@import "./variables.sass";
// ----------------------------------------
//
// ----------------------------------------
.hide
display: none !important;
// ----------------------------------------
//
// ----------------------------------------
.fake-link
color: $brand;
cursor: pointer;
&:hover
color: $brand;
// ----------------------------------------
//
// ----------------------------------------
//
.color-3 { color: #333; }
.color-6 { color: #666; }
.color-9 { color: #999; }
.color-c { color: #CCC; }
.color-f { color: #FFF; }
//
.bg-c { background-color: #CCCCCC; }
.bg-e6 { background-color: #E6E6E6; }
.bg-f0 { background-color: #F0F0F0; }
.bg-f5 { background-color: #F5F5F5; }
.bg-fa { background-color: #FAFAFA; }
.bg-f { background-color: #FFFFFF; }
// ----------------------------------------
//
// ----------------------------------------
@each $i in 12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,60
.fontsize-#{$i}
font-size: $i * 1px;
.font-bold
font-weight: bold;
.font-tahoma,
.font-number
font-family: "Tahoma";
.nowrap
white-space: nowrap;
// ----------------------------------------
// Line Height
// ----------------------------------------
@each $i in 0,1,2,3,4,5,6,7,8,9,10,12,15,20,25,30,32,35,40,50,60,80,100
.m#{$i}
margin: $i * 1px;
.ml#{$i}
margin-left: $i * 1px;
.mr#{$i}
margin-right: $i * 1px;
.mt#{$i}
margin-top: $i * 1px;
.mb#{$i}
margin-bottom: $i * 1px;
.p#{$i}
padding: $i * 1px;
.pl#{$i}
padding-left: $i * 1px;
.pr#{$i}
padding-right: $i * 1px;
.pt#{$i}
padding-top: $i * 1px;
.pb#{$i}
padding-bottom: $i * 1px;
@each $i in 50,60,70,80,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230,240,250,260,270,280,290,300,310,320,340,350,360,370,380,390,400,500,600,800
.w#{$i}
width: $i * 1px;
.h#{$i}
height: $i * 1px;
// ----------------------------------------
// Animation
// ----------------------------------------
// .animated {
// -webkit-animation-duration: 1s;
// -moz-animation-duration: 1s;
// animation-duration: 1s;
// -webkit-animation-fill-mode: both;
// -moz-animation-fill-mode: both;
// animation-fill-mode: both;
// }
// .animated.infinite {
// -webkit-animation-iteration-count: infinite;
// -moz-animation-iteration-count: infinite;
// animation-iteration-count: infinite;
// }

@ -0,0 +1,25 @@
// ----------------------------------------
//
// ----------------------------------------
$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;
$table-th-bg: #FAFAFA;
$bg: #F0F0F0;
$border: #d1d5da;
$warning: #FFB400;
$danger: #A62A22;
$success: #6CB12A;
$fail: #A62A22;
// ----------------------------------------
//
// ----------------------------------------
$font-family: "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", Georgia, "Times New Roman", Arial, sans-serif;

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

@ -0,0 +1,83 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { login } from '../../actions/account'
import { Link } from 'react-router-dom'
import Mock from 'mockjs'
import './LoginForm.css'
//
const mockUser = process.env.NODE_ENV === 'development'
? () => Mock.mock({
email: 'admin@rap2.com',
password: 'admin'
})
: () => ({
email: '',
password: ''
})
//
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>
<div className='footer'>
<button type='submit' className='btn btn-primary w140 mr20'>提交</button>
<Link to='/account/register'>注册</Link>
</div>
{this.props.auth.errMsg &&
<div className='alert alert-danger fade show' role='alert'>
{this.props.auth.errMsg}
</div>
}
</form>
</section>
)
}
handleSubmit = (e) => {
// console.log(this)
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)

@ -0,0 +1,22 @@
@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;

@ -0,0 +1,19 @@
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,69 @@
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)'
})
: () => ({
fullname: '',
email: '',
password: ''
})
//
class RegisterForm extends Component {
constructor (props) {
super(props)
this.state = mockUser()
}
render () {
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>
<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, () => {
history.push('/') // <Redirect to="/somewhere/else"/>
})
}
}
//
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({
onAddUser: addUser
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RegisterForm)

@ -0,0 +1,9 @@
.RegisterForm
width: 30rem;
margin: 0 auto;
.header
margin-bottom: 3rem;
.title
font-size: 2rem;
.body
.footer

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

@ -0,0 +1,66 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import Pagination from '../utils/Pagination'
import { addUser, deleteUser, fetchUserList } from '../../actions/account'
import './UserList.css'
//
const UserList = ({ history, match, location, users, onAddUser, onDeleteUser, onFetchUserList, tmpl = {} }) => (
<section className='UserList'>
<div className='header'>
<span className='title'>用户管理</span>
</div>
<nav className='toolbar clearfix'>
{/* <div className='float-left'>
<Link to='/account/register' className='btn btn-success w140'><span className=''>&#xe654;</span>注册用户</Link>
</div>
<div className='float-right'>
<button onClick={e => onFetchUserList(location.params)} className='btn btn-default'>R</button>
</div> */}
</nav>
<div className='body'>
<table className='table'>
<thead>
<tr>
<th>姓名</th>
<th>邮箱</th>
<th className='w100'>操作</th>
</tr>
</thead>
<tbody>
{users.data.map(user =>
<tr key={user.id}>
<td>
<Link to={`/repository?user=${user.id}`}>#{user.id} {user.fullname}</Link>
</td>
<td>{user.email}</td>
<td>
<span style={{ cursor: 'not-allowed' }}>
<Link to={match.url} onClick={e => onDeleteUser(user.id)} className='operation disabled'>删除</Link>
</span>
</td>
</tr>
)}
</tbody>
</table>
</div>
<div className='footer'>
<Pagination location={location} calculated={users.pagination} />
</div>
</section>
)
//
const mapStateToProps = (state) => ({
users: state.users
})
const mapDispatchToProps = ({
onAddUser: addUser,
onDeleteUser: deleteUser,
onFetchUserList: fetchUserList
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserList)

@ -0,0 +1,11 @@
.UserList
padding: 2rem;
.header
margin-bottom: 2rem;
.title
font-size: 2rem;
.toolbar
margin-bottom: 1rem;
.body
.table
.footer

@ -0,0 +1,162 @@
import React from 'react'
import { serve } from '../../relatives/services/constant'
import './API.css'
const ExampleJQuery = () => (
<div>
<ul>
<li>先引入jQuery插件</li>
<li>再引入基础插件</li>
<li>最后引入RAP jQuery插件</li>
</ul>
<h4>示例代码</h4>
<pre className='code-example'>
{
'<script src="jquery.min.js"></script>\n' +
'<script src="http://rap2api.taobao.org/app/plugin/:projectId"></script>\n' +
'<script src="http://rap2api.taobao.org/libs/jquery.rap.js"></script>\n' +
'$.ajax({\n' +
' url : \'/example/1501049256513\', // 自动拦截\n' +
' method : \'GET\',\n' +
' dataType : \'JSON\',\n' +
' success : function(data) {\n' +
' // 返回根据RAP文档及规则生成的mock数据\n' +
' $(\'#result\').html(JSON.stringify(data))\n' +
' }\n' +
'})\n'
}
</pre>
</div>
)
// DONE 2.3
class API extends React.Component {
constructor (props) {
super(props)
this.state = {
showExampleJQuery: false
}
}
render () {
return (
<section className='APIList'>
<div className='header'>
<span className='title'>接口</span>
</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>
<ul>
<li><code>{serve}/interface/get?id=:interfaceId</code></li>
</ul>
</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><a href='#' className='btn btn-secondary btn-sm ml8' onClick={
e => {
e.preventDefault()
this.setState((prevState, props) => {
return { showExampleJQuery: !prevState.showExampleJQuery }
})
}
}>用法</a></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>
</ul>
</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'>
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>response</code></td>
<td>获取单个接口的响应数据JSON</td>
</tr>
<tr>
<td><code>request</code></td>
<td>获取单个接口的请求数据JSON</td>
</tr>
</tbody>
</table>
</li>
<li><code>{serve}/app/mock/:repositoryId/:method/:url</code></li>
</ul>
</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'>
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>response</code></td>
<td>获取单个接口的响应模板JSON</td>
</tr>
<tr>
<td><code>request</code></td>
<td>获取单个接口的请求模板JSON</td>
</tr>
</tbody>
</table>
</li>
</ul>
</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'>
<thead>
<tr>
<th width='140'><code>scope</code></th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>response</code></td>
<td>获取单个接口的响应模板JSON Schema</td>
</tr>
<tr>
<td><code>request</code></td>
<td>获取单个接口的请求模板JSON Schema</td>
</tr>
</tbody>
</table>
</li>
</ul>
</div>
</div>
</section>
)
}
}
export default API

@ -0,0 +1,23 @@
.APIList
padding: 2rem;
> .header
margin-bottom: 2rem;
.title
font-size: 2rem;
> .body
> .API
margin-bottom: 2rem;
> .title
font-size: 1.6rem;
margin-bottom: 1rem;
> ul
padding-left: 2rem;
li
margin-bottom: .75rem;
.label
margin-right: .5rem;
code
padding: .5rem;
.code-example
padding: 2rem;
margin: 2rem;

@ -0,0 +1,108 @@
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)
console.log(result)
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,24 @@
@import "../../assets/variables.sass";
.Checker
padding: 2rem;
.card-mods, .card-itfs
.card-title
float: left;
width: 5rem;
font-size: 1.4rem;
a
float: left;
margin-right: 1rem;
width: 10rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #666;
a.active
color: $brand;
.card-result
margin-top: 1rem;
.card-title
font-size: 1.4rem;
margin-bottom: 1rem;

@ -0,0 +1,29 @@
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,15 @@
@import "../../assets/variables.sass";
.Footer
margin-top: 2rem;
padding: 2rem 1rem 1rem 1rem;
border-top: 1px solid $border;
text-align: center;
ul.friend_links
margin-top: .5rem;
list-style: none;
padding: 0;
li
display: inline-block;
margin-right: 1rem;

@ -0,0 +1,61 @@
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><a href='/account' className='name'>{user.fullname}</a></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)

@ -0,0 +1,38 @@
@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,19 @@
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,9 @@
@import "../../assets/variables.sass";
#nprogress
.bar
background-color: $brand;
.spinner
.spinner-icon
border-top-color: $brand;
border-left-color: $brand;

@ -0,0 +1,65 @@
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,130 @@
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 property = Object.assign({
name: schema.name,
type,
rule,
value: /Array|Object/.test(type) ? '' : schema.template,
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
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,166 @@
import React, { Component } from 'react'
import { PropTypes, connect, replace, StoreStateRouterLocationURI, _ } from '../../family'
import InterfaceEditorToolbar from './InterfaceEditorToolbar'
import InterfaceSummary from './InterfaceSummary'
import PropertyList from './PropertyList'
export const RequestPropertyList = (props) => (
<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,
handleAddMemoryProperty: PropTypes.func.isRequired,
handleAddMemoryProperties: PropTypes.func.isRequired,
handleDeleteMemoryProperty: PropTypes.func.isRequired,
handleChangeProperty: PropTypes.func.isRequired
}
getChildContext () {
return _.pick(this, Object.keys(InterfaceEditor.childContextTypes))
// return {
// handleLockInterface: this.handleLockInterface,
// handleUnlockInterface: this.handleUnlockInterface,
// handleSaveInterface: this.handleSaveInterface,
// handleAddMemoryProperty: this.handleAddMemoryProperty,
// handleAddMemoryProperties: this.handleAddMemoryProperties,
// handleDeleteMemoryProperty: this.handleDeleteMemoryProperty,
// handleChangeProperty: this.handleChangeProperty
// }
}
static mapPropsToState (props) {
let { auth, itf, properties } = props
return {
itf: { ...itf },
properties: properties.map(property => ({...property})),
editable: !!(itf.locker && (itf.locker.id === auth.id))
}
}
componentDidMount () {}
componentWillReceiveProps (nextProps) {
if (
nextProps.itf.id === this.state.itf.id &&
nextProps.itf.updatedAt === this.state.itf.updatedAt
) return
this.setState(InterfaceEditor.mapPropsToState(nextProps))
}
// 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) {}
constructor (props) {
super(props)
this.state = InterfaceEditor.mapPropsToState(props)
// { itf: {}, properties: [] }
}
render () {
let { auth, repository, mod, itf } = this.props
let { id, locker } = this.state.itf
if (!id) return null
return (
<article className='InterfaceEditor'>
<InterfaceEditorToolbar locker={locker} auth={auth} repository={repository} editable={this.state.editable} />
<InterfaceSummary repository={repository} mod={mod} itf={itf} active />
<RequestPropertyList properties={this.state.properties} editable={this.state.editable}
repository={repository} mod={mod} itf={this.state.itf} />
<ResponsePropertyList properties={this.state.properties} editable={this.state.editable}
repository={repository} mod={mod} itf={this.state.itf} />
</article>
)
}
handleAddMemoryProperty = (property, cb) => {
this.handleAddMemoryProperties([property], cb)
}
handleAddMemoryProperties = (properties, cb) => {
properties.forEach(item => {
if (item.memory === undefined) item.memory = true
if (item.id === undefined) item.id = _.uniqueId('memory-')
})
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()
let { onUpdateProperties } = this.context
onUpdateProperties(this.state.itf.id, this.state.properties, () => {
this.handleUnlockInterface()
})
}
handleLockInterface = () => {
let { onLockInterface } = this.context
let { itf } = this.props
onLockInterface(itf.id, () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
})
}
handleUnlockInterface = () => {
let { onUnlockInterface } = this.context
let { itf } = this.props
onUnlockInterface(itf.id, () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
})
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceEditor)

@ -0,0 +1,55 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import { GoPencil, GoGitPullRequest, GoX } 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
}
render () {
let { handleLockInterface, handleUnlockInterface, handleSaveInterface } = 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-success edit w130' onClick={handleLockInterface}><GoPencil /> 编辑</button>
</div>
)
}
}
const mapStateToProps = (state) => ({})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceEditorToolbar)

@ -0,0 +1,123 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, Mock } from '../../family'
import { SmartTextarea } from '../utils'
export const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
//
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' 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' 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' 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'>
<SmartTextarea 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 { 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,142 @@
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'
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
}
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}><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) => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
}
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
}
constructor (props) {
super(props)
this.state = { create: false }
}
render () {
let { auth, repository, mod, itfs = [], itf } = 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} />
</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) => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
handleSort = (e, sortable) => {
let { onSortInterfaceList } = this.context
onSortInterfaceList(sortable.toArray())
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceList)

@ -0,0 +1,105 @@
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'
class Previewer extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
scope: PropTypes.string.isRequired,
properties: PropTypes.array.isRequired,
itf: PropTypes.object.isRequired
}
render () {
let { label, scope, properties, itf } = this.props
// DONE 2.2
let scopedProperties = {
request: properties.map(property => ({ ...property })).filter(property => property.scope === 'request'),
response: properties.map(property => ({ ...property })).filter(property => property.scope === 'response')
}
let scopedTemplate = {
request: Tree.treeToJson(Tree.arrayToTree(scopedProperties.request)),
response: Tree.treeToJson(Tree.arrayToTree(scopedProperties.response))
}
let 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'))
}
let extraKeys = _.difference(scopedKeys.request, scopedKeys.response)
let scopedData = {
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]
let data = scopedData[scope]
// DONE 2.1 __root__
let 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]))
}
return (
<div className='Previewer row'>
<div className='result-template col-6'>
<div className='header'>
<span className='title'>{label}模板</span>
{scope === 'response'
? <Link to={`${serve}/app/mock/template/${itf.id}`} target='_blank'><GoLink className='fontsize-14' /></Link>
: 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
}, 2)
}</pre>
</div>
<div className='result-mocked col-6'>
<div className='header'>
<span className='title'>{label}数据</span>
{scope === 'response'
? <Link to={`${serve}/app/mock/data/${itf.id}`} target='_blank'><GoLink className='mr6 fontsize-14' /></Link>
: null}
<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>
</div>
{scope === 'response'
? <div className='result-valid col-12'>
{!valid.length
? <span><GoBeer className='mr6 fontsize-20' />模板与数据匹配 </span>
: <span><GoBug className='mr6 fontsize-20' />模板与数据不匹配</span>
}
</div>
: null
}
</div>
)
}
remock = (e) => {
e.preventDefault()
let target = e.currentTarget.firstChild
target.classList.add('animated')
target.classList.add('rotateIn')
this.forceUpdate()
}
removeAnimateClass = (e) => {
let target = e.currentTarget
target.classList.remove('animated')
target.classList.remove('rotateIn')
}
}
export default Previewer

@ -0,0 +1,72 @@
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'
class InterfaceSummary extends Component {
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
}
render () {
let { repository = {}, mod = {}, itf = {} } = this.props
if (!itf.id) return null
return (
<div className='InterfaceSummary'>
<div className='header'>
<span className='title'>
{mod.name}
<span className='slash'> / </span>
{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>
<Link to={`${serve}/app/mock/${repository.id}/${itf.method}/${itf.url}`} target='_blank'>{itf.url}</Link>
</li>
<li><span className='label'>类型</span>{itf.method}</li>
{itf.description &&
<li><span className='label'>简介</span>{itf.description}</li>
}
</ul>
</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 = (e) => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
}
export default InterfaceSummary

@ -0,0 +1,94 @@
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' 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 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
})
onAddOrUpdateModule(mod, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleForm)

@ -0,0 +1,126 @@
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))
})
}
}
}
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,137 @@
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' 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' 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' 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' 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 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,244 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, replace, StoreStateRouterLocationURI } 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'
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>
{/* TODO 2.3 规则编辑器 */}
<div className='th rule'>
生成规则
<Link to='https://github.com/nuysoft/Mock/wiki/Syntax-Specification' className='helper' target='_blank'><GoQuestion /></Link>
</div>
<div className='th value'>初始值</div>{/* 对象和数组也允许设置初始值 */}
<div className='th desc'>简介</div>
</div>
</div>
)
}
}
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'>
{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}</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'>
{!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
}
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 scopedProperties = properties.map(property => ({ ...property })).filter(property => property.scope === scope)
let { editable } = this.props // itf.locker && (itf.locker.id === auth.id)
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 = () => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
handleDeleteMemoryProperty = (e, property) => {
e.preventDefault()
let { handleDeleteMemoryProperty } = this.context
handleDeleteMemoryProperty(property)
}
handleSortProperties = (e, sortable) => {
// let { onSortPropertyList } = this.context
// onSortPropertyList(sortable.toArray())
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,157 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, replace, _ } from '../../family'
import { serve } from '../../relatives/services/constant'
import { RModal, Spin } from '../utils'
import RepositoryForm from '../repository/RepositoryForm'
import RepositorySearcher from './RepositorySearcher'
import ModuleList from './ModuleList'
import InterfaceList from './InterfaceList'
import InterfaceEditor from './InterfaceEditor'
import DuplicatedInterfacesWarning from './DuplicatedInterfacesWarning'
import { addRepository, updateRepository, clearRepository } from '../../actions/repository'
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 } from 'react-icons/lib/go'
import './RepositoryEditor.css'
// DONE 2.1 import Spin from '../utils/Spin'
// TODO 2.2
// DONE 2.2
// TODO 2.1
//
class RepositoryEditor extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
onClearRepository: PropTypes.func.isRequired
}
static childContextTypes = {
onAddRepository: PropTypes.func.isRequired,
onUpdateRepository: PropTypes.func.isRequired,
onAddModule: PropTypes.func.isRequired,
onUpdateModule: PropTypes.func.isRequired,
onDeleteModule: PropTypes.func.isRequired,
onSortModuleList: PropTypes.func.isRequired,
onAddInterface: PropTypes.func.isRequired,
onUpdateInterface: PropTypes.func.isRequired,
onDeleteInterface: PropTypes.func.isRequired,
onLockInterface: PropTypes.func.isRequired,
onUnlockInterface: PropTypes.func.isRequired,
onSortInterfaceList: PropTypes.func.isRequired,
onAddProperty: PropTypes.func.isRequired,
onUpdateProperty: PropTypes.func.isRequired,
onUpdateProperties: PropTypes.func.isRequired,
onDeleteProperty: PropTypes.func.isRequired,
onSortPropertyList: PropTypes.func.isRequired
}
getChildContext () {
return _.pick(this.props, Object.keys(RepositoryEditor.childContextTypes))
}
constructor (props) {
super(props)
this.state = {
update: false
}
}
render () {
let { location: { params }, auth, 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 每次获取仓库都显示加载动画不合理应该只在初始加载时显示动画
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 || []
let 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)
return (
<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>{repository.name}</span>
</span>
<div className='toolbar'>
{/* 编辑权限:拥有者或者成员 */}
{isOwned || isJoined
? <span className='fake-link edit' onClick={e => 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>
<Link to={`${serve}/app/plugin/${repository.id}`} target='_blank' className='api'><GoPlug /> 插件</Link>
<Link to={`${serve}/repository/get?id=${repository.id}`} target='_blank' className='api'><GoDatabase /> 数据</Link>
<Link to={`${serve}/test/test.plugin.jquery.html?id=${repository.id}`} target='_blank' className='api'><GoJersey /> 测试</Link>
</div>
<RepositorySearcher repository={repository} />
<div className='desc'>{repository.description}</div>
<DuplicatedInterfacesWarning repository={repository} />
</div>
<div className='body'>
<ModuleList mods={repository.modules} repository={repository} mod={mod} />
<div className='InterfaceWrapper'>
<InterfaceList itfs={mod.interfaces} repository={repository} mod={mod} itf={itf} />
<InterfaceEditor itf={itf} properties={properties} mod={mod} repository={repository} />
</div>
</div>
</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()
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth,
repository: state.repository
})
const mapDispatchToProps = ({
onAddRepository: addRepository,
onUpdateRepository: updateRepository,
onClearRepository: clearRepository,
onAddModule: addModule,
onUpdateModule: updateModule,
onDeleteModule: deleteModule,
onSortModuleList: sortModuleList,
onAddInterface: addInterface,
onUpdateInterface: updateInterface,
onDeleteInterface: deleteInterface,
onLockInterface: lockInterface,
onUnlockInterface: unlockInterface,
onSortInterfaceList: sortInterfaceList,
onAddProperty: addProperty,
onUpdateProperty: updateProperty,
onUpdateProperties: updateProperties,
onDeleteProperty: deleteProperty,
onSortPropertyList: sortPropertyList
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RepositoryEditor)

@ -0,0 +1,368 @@
@import "../../assets/variables.sass";
.RepositoryEditor
> .header
position: relative;
padding: 2rem;
background-color: #fafbfc;
> .title
font-size: 2rem;
margin-right: 1rem;
.slash
color: #999;
> .toolbar
display: inline-block;
a, .fake-link
margin-right: 1rem;
> .desc
margin-top: .5rem;
color: #666;
> .body
// padding: 0 2rem;
.RepositorySearcher.dropdown
position: absolute;
top: 2rem;
right: 2rem;
left: auto;
> .dropdown-input
margin-bottom: 0;
width: 20rem;
> .dropdown-menu
position: absolute;
right: 0;
left: auto;
display: block;
min-width: 100%;
max-height: 50rem;
overflow: scroll;
a.dropdown-item
&.dropdown-item-module
padding-left: 1.5rem;
color: #333;
&.dropdown-item-interface
padding-left: 1.5rem + 2.5rem;
color: #333;
&.dropdown-item-property
padding-left: 1.5rem + 2.5rem + 2.5rem;
color: #666;
> .label
margin-right: .5rem;
color: #666;
> .dropdown-item-clip
margin-right: .5rem;
.highlight
font-weight: bold;
color: $brand;
&:active
color: #FFF;
> .label
color: #FFF;
> .dropdown-item-clip
.highlight
color: #FFF;
.DuplicatedInterfacesWarning
margin-top: 1rem;
.alert.alert-warning
margin-bottom: .5rem;
.title
margin-right: 1rem;
.icon
font-size: 1.4rem;
margin-right: .5rem;
.msg
font-weight: bold;
margin-right: 1rem;
.itf
a
margin-right: 1rem;
.ModuleList
margin: 0 0 1.5rem 0;
padding: 0 2rem;
list-style: none;
border-bottom: 1px solid #e1e4e8;
background-color: #fafbfc;
> li
position: relative;
display: block;
float: left;
margin-bottom: -1px;
padding: .8rem 1.2rem;
border: 1px solid transparent;
border-width: 3px 1px 0px 1px;
border-radius: .4rem .4rem 0 0;
&.active
// border: 1px solid $border;
border-bottom-color: transparent;
background-color: white;
cursor: default;
border-color: #e36209 #e1e4e8 transparent #e1e4e8;
&.active:hover
background-color: white;
> .Module
position: relative;
a.name
color: #586069;
.toolbar
// float: right;
display: inline-block;
a, .fake-link
margin-left: 0.5rem;
font-size: 1.4rem;
color: #999;
&:hover
color: $brand;
> li:hover > .Module
.toolbar
display: inline-block;
> li.active > .Module
a.name
color: #333;
.toolbar
display: inline-block;
// font-size: 1.4rem;
.InterfaceWrapper
display: flex;
flex-direction: row;
padding: 0 2rem;
.InterfaceList
flex-basis: 16rem;
flex-shrink: 0;
.InterfaceEditor
flex-grow: 1;
.InterfaceList
ul.body
margin: 0;
padding: 0;
border: 1px solid #d1d5da;
border-radius: .4rem;
list-style: none;
> li
position: relative;
padding: 1rem 1rem;
border-bottom: 1px solid #d1d5da;
&:last-child
border-bottom: 0;
.Interface
position: relative;
padding-right: 4rem;
.name
position: relative;
float: left;
// width: 10rem;
max-width: 30rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.toolbar
display: none;
position: absolute;
right: 0;
top: 0;
font-size: 1.4rem;
a, .fake-link
margin-left: 0.5rem;
color: #999;
&:hover
color: $brand;
.locked
font-size: 1.4rem;
color: $warning;
> li:hover
.toolbar
display: block;
> li.active
.Interface
.name a
color: #333;
&:hover
color: #333;
.toolbar
display: block;
> li.active
&::before
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
content: "";
background-color: #e36209;
.footer
padding: 0.75rem 1rem;
.InterfaceEditor
margin-left: 2rem;
position: relative;
.InterfaceEditorToolbar
position: absolute;
top: 0;
right: 0;
text-align: center;
.btn.edit, .btn.save, .btn.cancel, .locker-success, .locker-warning
display: block;
margin-bottom: 0.5rem;
width: 12rem;
.locker-success
display: inline-block;
padding: 0.5rem 1.5rem;
border-radius: 0.4rem;
color: white;
background-color: $success;
.locker-warning
@extend .locker-success;
background-color: $warning;
.InterfaceSummary
margin-bottom: 2rem;
padding-right: 13rem;
> .header
margin-bottom: .5rem;
> .title
font-size: 1.6rem;
margin-right: 1rem;
.slash
color: #999;
a.edit, a.delete
margin-right: .5rem;
ul.body
color: #666;
margin: 0;
padding: 0;
list-style: none;
> li
margin-bottom: .2rem;
.label
color: #666;
margin-right: .3rem;
.PropertyList
margin-bottom: 3rem;
> .header
margin-bottom: 1rem;
> .title
font-size: 1.6rem;
margin-right: 1rem;
> .toolbar
float: right;
.preview
margin: 0;
input
margin-right: .5rem;
> .body
margin-bottom: 1rem;
> .footer
> .Previewer
margin-top: 1rem;
> .result-template,
> .result-mocked
> .header
margin-bottom: .5rem;
.title
margin-right: .75rem
> pre.body
> .result-valid
padding-top: 2.5rem;
text-align: center;
color: #999;
.SortableTreeTable
.SortableTreeTableHeader,
.SortableTreeTableRow
.flex-row
display: flex;
.thtd
border: 1px solid #eceeef;
flex-grow: 1;
//
display: flex;
align-items: center;
flex-direction: row;
margin-right: -1px;
.th
@extend .thtd;
padding: .75rem;
font-weight: bold;
.td
@extend .thtd;
padding: .5rem .75rem;
margin-bottom: -1px;
.th, .td
&.operations
width: 4.5rem;
min-width: 4.5rem;
&.name
width: 20rem;
flex-grow: 3;
&.type
width: 7rem;
&.rule
width: 10rem;
&.value
width: 10rem;
&.desc
width: 15rem;
.th
.helper
margin-left: .5rem;
color: #999;
&:hover
color: $brand;
.td
&.operations
padding: .5rem .75rem;
height: auto;
line-height: 1;
justify-content: flex-end;
a
color: #999;
margin-right: .5rem;
&:last-child
margin-right: 0;
&:hover
color: $brand;
&.payload
padding: .5rem .75rem;
height: auto;
line-height: 1.5;
&.payload.name
@for $i from 0 through 42
&.depth-#{$i}
padding-left: $i * 1rem + 0.75rem;
&.payload.value
word-break: break-word;
.SortableTreeTable.editable
.flex-row
.td
&.payload
padding: 0;
&.payload.name
@for $i from 0 through 10
&.depth-#{$i}
padding-left: $i * 1rem;
input.editable,
select.editable,
textarea.editable
margin: 0;
padding: .5rem .75rem;
width: 100%;
height: auto;
border: none;
border-radius: 0;
line-height: 1.5;
outline: none;
box-shadow: none;
background-color: transparent;
.Importer
.CodeMirror
height: 30rem;
textarea.result
font-family: Menlo, Monaco, 'Courier New', monospace

@ -0,0 +1,132 @@
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,53 @@
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,59 @@
@import "../../assets/variables.sass";
.Home
padding: 2rem;
.Maiden
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
.btn
font-size: 1.6rem;
.card
margin-bottom: 2rem;
border: 1px solid $border;
border-radius: .5rem;
overflow: hidden;
.card-header
border-bottom: 1px solid $border;
font-size: 1.6rem;
.card-block
> p:last-child
margin-bottom: 0;
a:hover
border-bottom: 1px solid $brand;
.card:last-child
// margin-bottom: 0;
.card.Logs
.Log
padding: 1rem 0;
border-bottom: 1px solid #eff3f6;
.Log-body
float: left;
.Log-user
// margin-right: .5rem;
.Log-avatar
margin-right: .75rem;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
.Log-user-link
margin-right: .5rem;
.Log-type
margin-right: .5rem;
.Log-target
margin-right: .5rem;
.slash
color: #999;
.Log-footer
float: right;
margin-left: .5rem;
padding: .4rem 0;
.Log-fromnow
color: #666;
// padding: 4rem 1rem;
// text-align: center;
// font-size: 4rem;
.Log:last-child
border-bottom: 0;

@ -0,0 +1,30 @@
import React from 'react'
import { Link } from '../../family'
import { Spin } from '../utils'
const JoinedRepositoriesCard = ({ repositories }) => (
<div className='card'>
<div className='card-header'>我加入的仓库</div>
{repositories.fetching ? <Spin /> : (
<div className='card-block'>
{repositories.data.slice(0, 10).map(repository =>
<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>
: null
}
</div>)
}
</div>
)
const JoinedRepositoryLink = ({ repository }) => (
<Link to={`/repository/editor?id=${repository.id}`}>
<span>{repository.organization ? repository.organization.name : repository.owner.fullname}</span>
<span> / </span>
<span>{repository.name}</span>
</Link>
)
export default JoinedRepositoriesCard

@ -0,0 +1,228 @@
import React from 'react'
import { Link, moment } from '../../family'
import { Spin } from '../utils'
// DONE 2.3 LogView
// 1. targe type
// 2. 线线 /~~~~//~~~~
// 3. `log.creator`
// 1.
// 2.
// 4. UserAvatar HTML
// 5. FromNow
const UserAvatar = ({ user }) => (
user
? <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 LogUserView = ({ user }) => {
return (
<span className='Log-user'>
<UserAvatar user={user} />
<UserLink user={user} />
</span>
)
}
const LogTypeView = ({ type }) => {
let typeName = {
create: '创建了',
update: '修改了',
delete: '删除了',
lock: '锁定了',
unlock: '释放了',
join: '加入了',
exit: '退出了'
}[type]
return <span className='Log-type'>{typeName}</span>
}
const LogTargetView = ({ log }) => {
let 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) {
case 'organization':
return !log.organization.deletedAt
? <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>
: <s>{log.repository.name}</s>
case 'module':
return (
<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>
{!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>
)
case 'interface':
return (
<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>
{!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>
{!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>
}
</span>
)
default:
return undefined
}
}
const FromNow = ({ date }) => (
<i className='Log-fromnow'>{moment(date).fromNow()}</i>
)
const LogView = ({ log }) => {
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} />
}
return (
<div className='Log clearfix'>
<div className='Log-body'>
<LogUserView user={log.user} />
<LogTypeView type={log.type} />
<LogTargetView log={log} />
</div>
<div className='Log-footer'>
<FromNow date={log.createdAt} />
</div>
</div>
)
}
// DONE 2.3 `log.creator`
//
const JoinLogView = ({ log }) => {
return (
<div className='Log clearfix'>
<div className='Log-body'>
<LogUserView user={log.creator} />
<span className='Log-type'>邀请</span>
<UserLink user={log.user} />
<span className='Log-type'>加入了</span>
<LogTargetView log={log} />
</div>
<div className='Log-footer'>
<FromNow date={log.createdAt} />
</div>
</div>
)
}
//
const ExitLogView = ({ log }) => {
return (
<div className='Log clearfix'>
<div className='Log-body'>
<LogUserView user={log.creator} />
<span className='Log-type'></span>
<UserLink user={log.user} />
<span className='Log-type'>移出了</span>
<LogTargetView log={log} />
</div>
<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
if (log.organization) { //
targetName = log.organization.name
targetLink = !log.organization.deletedAt
? <Link to={`/organization/repository?organization=${log.organization.id}`}>{targetName}</Link>
: targetName
}
if (log.repository && !log.module && !log.interface) { //
targetName = log.repository.name
targetLink = !log.repository.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}`}>{targetName}</Link>
: targetName
}
if (log.repository && log.module && !log.interface) { //
targetName = `${log.repository.name} / ${log.module.name}`
targetLink = !log.repository.deletedAt && !log.module.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}&mod=${log.module.id}`}>{targetName}</Link>
: targetName
}
if (log.repository && log.module && log.interface) { //
targetName = `${log.repository.name} / ${log.module.name} / ${log.interface.name}`
targetLink = !log.repository.deletedAt && !log.module.deletedAt && !log.interface.deletedAt
? <Link to={`/repository/editor?id=${log.repository.id}&mod=${log.module.id}&itf=${log.interface.id}`}>{targetName}</Link>
: targetName
}
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
case 'update':
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
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>
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
default:
return null
// 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> */}
{logs.fetching ? <Spin /> : (
<div className='card-block'>
{logs.data.map(log =>
<LogView key={log.id} log={log} />
)}
</div>
)}
</div>
)
export default LogsCard

@ -0,0 +1,29 @@
import React from 'react'
import { Link } from '../../family'
import { Spin } from '../utils'
const OwnedRepositoriesCard = ({ repositories }) => (
<div className='card'>
<div className='card-header'>我拥有的仓库</div>
{repositories.fetching ? <Spin /> : (
<div className='card-block'>
{repositories.data.slice(0, 10).map(repository =>
<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>
: null
}
</div>)
}
</div>
)
const OwnedRepositoryLink = ({ repository }) => (
<Link to={`/repository/editor?id=${repository.id}`}>
<span>{repository.organization ? repository.organization.name + ' / ' : ''}</span>
<span>{repository.name}</span>
</Link>
)
export default OwnedRepositoriesCard

@ -0,0 +1,38 @@
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'
//
class JoinedOrganizationList extends Component {
static contextTypes = contextTypes
static childContextTypes = childContextTypes
getChildContext = getChildContext
render () {
let { location, match, organizations } = this.props
return (
<section className='OrganizationListWrapper'>
<nav className='toolbar clearfix'>
<OrganizationsTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
<CreateButton />
</nav>
<div className='body'>
<OrganizationListWithSpin name={location.params.name} organizations={organizations} />
</div>
<div className='footer'>
<PaginationWithLocation calculated={organizations.pagination} />
</div>
</section>
)
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
organizations: state.organizations
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(JoinedOrganizationList)

@ -0,0 +1,47 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { contextTypes, childContextTypes, getChildContext, CreateButton, OrganizationsTypeDropdown, SearchGroup, OrganizationListWithSpin, mapDispatchToProps } from './OrganizationListParts'
import _ from 'lodash'
import moment from 'moment'
import './Organization.css'
//
class JoinedOrganizationList extends Component {
static contextTypes = contextTypes
static childContextTypes = childContextTypes
getChildContext = getChildContext
render () {
let { location, match, organizations } = this.props
return (
<section className='OrganizationListWrapper'>
<nav className='toolbar clearfix'>
<OrganizationsTypeDropdown url={match.url} />
<SearchGroup name={location.params.name} />
<CreateButton />
</nav>
<div className='body'>
<OrganizationListWithSpin name={location.params.name} organizations={organizations} />
</div>
<div className='footer'>
{/* 没有分页! */}
</div>
</section>
)
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
joiner: state.auth,
organizations: {
fetching: state.ownedOrganizations.fetching || state.joinedOrganizations.fetching,
data: _.uniqBy([...state.ownedOrganizations.data, ...state.joinedOrganizations.data], 'id')
.sort((a, b) => {
return moment(b.updatedAt).diff(a.updatedAt)
})
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(JoinedOrganizationList)

@ -0,0 +1,98 @@
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,56 @@
.OrganizationListWrapper
padding: 2rem;
> .header
margin-bottom: 2rem;
.title
font-size: 2rem;
> .toolbar
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e1e4e8;
select.OrganizationsTypeDropdown
margin-right: .5rem;
font-size: 1.4rem;
button.OrganizationCreateButton
padding: .4rem 1rem;
font-size: 1.4rem;
.form-control
margin-bottom: 0;
> .body
.table
> .footer
.OrganizationList
.Organization.card
margin-bottom: 1rem;
// transition: box-shadow .15s ease-out;
// &:hover
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.18);
.card-block
> .header
.title
margin-bottom: .5rem;
font-size: 1.4rem;
.toolbar
float: right;
display: none;
margin-left: 1rem;
> .body
.desc
margin-bottom: .5rem;
color: #666;
.members
.avatar
margin-right: .2rem;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
.avatar.owner
margin-right: 1rem;
.popover
white-space: nowrap;
.card-block:hover
> .header
.toolbar
display: inline-block

@ -0,0 +1,132 @@
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'
//
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'>
<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,35 @@
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,184 @@
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) => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
}
// 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))
// let uri = StoreStateRouterLocationURI(store)
// store.dispatch(replace(uri.href()))
}
}
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,49 @@
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,38 @@
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,47 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { contextTypes, childContextTypes, getChildContext, CreateButton, RepositoriesTypeDropdown, SearchGroup, mapDispatchToProps, RepositoryListWithSpin, PaginationWithLocation } from './RepositoryListParts'
import _ from 'lodash'
import moment from 'moment'
import './Repository.css'
//
class JoinedRepositoryList extends Component {
static contextTypes = contextTypes
static propTypes = {}
static childContextTypes = childContextTypes
getChildContext = getChildContext.bind(this)
render () {
let { location, match, auth, repositories, create } = this.props
return (
<section className='RepositoryListWrapper'>
<nav className='toolbar clearfix'>
{(!location.params.user || +location.params.user === auth.id) ? <RepositoriesTypeDropdown url={match.url} /> : null}
<SearchGroup name={location.params.name} />
{(!location.params.user || +location.params.user === auth.id) ? <CreateButton owner={auth} create={create} callback='/repository/joined' /> : null}
</nav>
<div className='body'>
<RepositoryListWithSpin name={location.params.name} repositories={repositories} />
</div>
<div className='footer'>
<PaginationWithLocation calculated={repositories.pagination} />
</div>
</section>
)
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
repositories: {
fetching: state.ownedRepositories.fetching || state.joinedRepositories.fetching,
data: _.uniqBy([...state.ownedRepositories.data, ...state.joinedRepositories.data], 'id')
.sort((a, b) => {
return moment(b.updatedAt).diff(a.updatedAt)
})
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(JoinedRepositoryList)

@ -0,0 +1,92 @@
import React, { Component } from 'react'
import { connect, PropTypes, Link, replace, StoreStateRouterLocationURI, moment } from '../../family'
import { RModal } from '../utils'
import { serve } from '../../relatives/services/constant'
import RepositoryForm from './RepositoryForm'
import { GoRepo, GoPencil, GoPlug, GoTrashcan, GoPerson, GoOrganization } from 'react-icons/lib/go'
// DONE 2.1 iconfont => octicons
class Repository extends Component {
static contextTypes = {
store: PropTypes.object,
location: PropTypes.object,
onDeleteRepository: PropTypes.func
}
constructor (props) {
super(props)
this.state = { update: false }
}
render () {
let { location } = this.context
let { auth, repository, editor } = this.props
return (
<div className='Repository card'>
<div className='card-block'>
<div className='name'>
<GoRepo className='mr6 color-9' />
<Link to={`${editor}?id=${repository.id}`}>{repository.name}</Link>
</div>
<div className='desc'>
{repository.description}
</div>
{/* TODO 2.x 成员列表参考 ProductHunt仓库成员不怎么重要暂时不现实 */}
{/* <div className='members'>
{repository.members.map(user =>
<img key={user.id} alt={user.id} title={user.fullname} src={`https://work.alibaba-inc.com/photo/${user.id}.220x220.jpg`} className='avatar' />
)}
</div> */}
<div className='toolbar'>
<Link to={`${serve}/app/plugin/${repository.id}`} target='_blank'><GoPlug /></Link>
{/* 编辑权限:拥有者或者成员 */}
{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
}
<RModal when={this.state.update} onClose={e => this.setState({ update: false })} onResolve={this.handleUpdateRepository}>
<RepositoryForm title='编辑仓库' repository={repository} />
</RModal>
{/* 删除权限:个人仓库 */}
{!repository.organization && repository.owner.id === auth.id
? <Link to={location.pathname + location.search} onClick={this.handleDeleteRepository}><GoTrashcan /></Link>
: null
}
</div>
</div>
<div className='card-block card-footer'>
{repository.organization
? <span className='ownername'><GoOrganization /> {repository.organization.name}</span>
: <span className='ownername'><GoPerson /> {repository.owner.fullname}</span>
}
<span className='fromnow'>{moment(repository.updatedAt).fromNow()}更新</span>
</div>
</div>
)
}
handleDeleteRepository = (e) => {
e.preventDefault()
let { repository } = this.props
let message = `仓库被删除后不可恢复,并且会删除相关的模块和接口!\n确认继续删除『#${repository.id} ${repository.name}』吗?`
if (window.confirm(message)) {
let { onDeleteRepository } = this.context
onDeleteRepository(repository.id)
let { store, location: { pathname, hash, search } } = this.context
store.dispatch(replace(pathname + hash + search))
}
}
handleUpdateRepository = (e) => {
let { store } = this.context
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Repository)

@ -0,0 +1,74 @@
@import "../../assets/variables.sass";
.RepositoryListWrapper
padding: 2rem;
> .header
margin-bottom: 1rem;
.title
font-size: 2rem;
> .toolbar
margin-bottom: 1rem;
padding-bottom: .5rem;
border-bottom: 1px solid #e1e4e8;
select.RepositoriesTypeDropdown
margin-right: .5rem;
margin-bottom: .5rem;
font-size: 1.4rem;
button.RepositoryCreateButton
padding: .4rem 1rem;
font-size: 1.4rem;
.form-control
margin-bottom: .5rem;
> .body
margin-bottom: 2rem;
> .footer
.RepositoryList
.Repository.card
margin-bottom: 1rem;
// transition: box-shadow .15s ease-out;
// &:hover
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.18);
.card-block
position: relative;
.name
font-size: 1.4rem;
white-space: nowrap;
overflow: hidden;
.desc
margin-top: 1rem;
height: 6rem;
overflow: hidden;
color: #666;
.members
display: none;
height: 2.5rem;
.avatar
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
.toolbar
// display: none
position: absolute;
top: 1.2rem;
right: 1.25rem;
background-color: white;
> a, > .fake-link
margin: 0 0 0 0.5rem;
font-size: 1.4rem;
color: #999;
&:hover
color: $brand;
.card-block.card-footer
padding-top: 0;
background-color: white;
border-top: none;
color: #666;
.ownername
float: left;
.fromnow
float: right;
.Repository.card:hover
.card-block
.toolbar
display: block;

@ -0,0 +1,155 @@
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 { GoInfo } from 'react-icons/lib/go'
//
// DONE 2.1
const mockRepository = process.env.NODE_ENV === 'development'
? () => mock({
name: '仓库@CTITLE(6)',
description: '@CPARAGRAPH',
members: [],
ownerId: undefined,
organizationId: undefined,
collaboratorIds: []
})
: () => ({
name: '',
description: '',
members: [],
ownerId: undefined,
organizationId: undefined,
collaboratorIds: []
})
//
//
// TODO 2.x
// DONE 2.2
class RepositoryForm extends Component {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddRepository: PropTypes.func.isRequired,
onUpdateRepository: PropTypes.func.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired,
organization: PropTypes.object,
repository: PropTypes.object
}
constructor (props) {
super(props)
let { repository } = props
this.state = repository ? {
...repository,
collaboratorIds: repository.collaborators.map(item => item.id),
newOwner: repository.owner
} : mockRepository()
}
render () {
const { rmodal } = this.context
let { auth } = 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={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'>
<input name='name' value={this.state.name} onChange={e => this.setState({ name: e.target.value })} className='form-control' placeholder='Name' spellCheck='false' autoComplete='off' autoFocus='true'
required data-parsley-trigger='change keyup' data-parsley-maxlength='256' />{/* w280 */}
</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 className='form-group row'>
{/* DONE 2.1 帮助信息:仓库 ID 用逗号分隔,例如 1,2,3 */}
<label className='col-sm-2 control-label'>协同仓库</label>
<div className='col-sm-10'>
{/* TODO 2.2 CollaboratorsInput */}
<input name='name' value={this.state.collaboratorIds.join(',')} onChange={e => this.setState({ collaboratorIds: e.target.value.split(',') })} className='form-control' placeholder='Collaborator Ids' spellCheck='false' autoComplete='off' />
<div className='mt6 color-9'>
<GoInfo className='mr3' />
指定与哪些仓库共享接口仓库 ID 之间用逗号分隔例如 <code>1,2,3</code></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>
</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
onAddOrUpdateRepository(repository, () => {
let { rmodal } = this.context
if (rmodal) rmodal.resolve()
})
}
}
//
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RepositoryForm)

@ -0,0 +1,30 @@
import React, { Component } from 'react'
import { PropTypes } from '../../family'
import Repository from './Repository'
class RepositoryList extends Component {
static propTypes = {
name: PropTypes.string, //
repositories: PropTypes.array.isRequired, //
editor: PropTypes.string.isRequired //
}
render () {
let { name, repositories, editor } = this.props
if (!repositories.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='RepositoryList row'>
{repositories.map(repository =>
<div key={repository.id} className='col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3'>
<Repository repository={repository} editor={editor} />
</div>
)}
</div>
)
}
}
export default RepositoryList

@ -0,0 +1,153 @@
import React, { Component } from 'react'
import { PropTypes, push, replace, URI, StoreStateRouterLocationURI } from '../../family'
import { Spin, RModal, Pagination } from '../utils'
import RepositoryList from './RepositoryList'
import RepositoryForm from './RepositoryForm'
import { addRepository, updateRepository, deleteRepository } from '../../actions/repository'
import { GoRepo } from 'react-icons/lib/go'
export const contextTypes = {
store: PropTypes.object
}
export const childContextTypes = {
history: PropTypes.object,
location: PropTypes.object,
match: PropTypes.object,
onAddRepository: PropTypes.func,
onUpdateRepository: PropTypes.func,
onDeleteRepository: PropTypes.func
}
export function getChildContext () {
let { history, location, match, onAddRepository, onUpdateRepository, onDeleteRepository } = this.props
return { history, location, match, onAddRepository, onUpdateRepository, onDeleteRepository }
}
export const mapDispatchToProps = ({
onAddRepository: addRepository,
onUpdateRepository: updateRepository,
onDeleteRepository: deleteRepository
})
export class CreateButton extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
create: PropTypes.bool,
callback: PropTypes.string,
organization: PropTypes.object
}
constructor (props) {
super(props)
this.state = { create: !!props.create }
}
render () {
let { organization } = this.props
return (
<span className='float-right ml10'>
{/* DONE 2.1 √我加入的仓库、X所有仓库 是否应该有 新建仓库 */}
<button className='RepositoryCreateButton btn btn-success' onClick={e => this.setState({ create: true })}>
<GoRepo /> 新建仓库
</button>
<RModal when={this.state.create} onClose={e => this.setState({ create: false })} onResolve={this.handleUpdate}>
<RepositoryForm title='新建仓库' organization={organization} />
</RModal>
</span>
)
}
handleUpdate = (e) => {
let { callback } = this.props
let { store } = this.context
if (callback) {
store.dispatch(replace(callback))
} else {
let uri = StoreStateRouterLocationURI(store)
store.dispatch(replace(uri.href()))
}
}
}
// TODO 2.2 <select> => <Dropdown>
export class RepositoriesTypeDropdown extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
render () {
let { url } = this.props
return (
<select className='RepositoriesTypeDropdown form-control float-left w160 mr12' value={url} onChange={e => this.handlePush(e.target.value)} size='1'>
<option value='/repository/joined'>我拥有和加入的仓库</option>
<option value='/repository/all'>所有仓库</option>
</select>
)
}
handlePush = (url) => {
let { store } = this.context
store.dispatch(push(url))
// let uri = StoreStateRouterLocationURI(store)
// store.dispatch(replace(uri.href()))
}
}
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()))
}
}
export const RepositoryListWithSpin = ({ name, repositories }) => (
repositories.fetching
? <Spin />
: <RepositoryList name={name} repositories={repositories.data} editor='/repository/editor' />
)
export const OrganizationRepositoryListWithSpin = ({ name, repositories }) => (
repositories.fetching
? <Spin />
: <RepositoryList name={name} repositories={repositories.data} editor='/organization/repository/editor' />
)
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,109 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { RChart } from '../utils/'
import './Status.css'
// TODO 2.3 线 线
// TODO 2.3
// TODO 2.3 线
// TODO 2.3 线
class Status extends Component {
static adapt (list, label) {
return {
labels: list.map(item => item.label),
datasets: [{
label: label || '-',
data: list.map(item => item.value),
backgroundColor: RChart.COLORS.blue,
borderColor: RChart.COLORS.blue,
borderWidth: 1,
fill: false
}]
}
}
render () {
let { counter, users, organizations, repositories, interfaces } = this.props
let { analyticsRepositoriesCreated, analyticsRepositoriesUpdated, analyticsUsersActivation, analyticsRepositoriesActivation } = this.props
// analyticsRepositoriesCreated = analyticsRepositoriesCreated.map((item, index, array) => ({
// label: item.label,
// value: array.slice(0, index + 1).reduce((sum, item) => {
// return sum + item.value
// }, 0)
// })) //
analyticsUsersActivation = analyticsUsersActivation.map(item => ({
label: item.fullname || item.empId || item.userId,
value: item.value
}))
analyticsRepositoriesActivation = analyticsRepositoriesActivation.map(item => ({
label: item.name || item.repositoryId,
value: item.value
}))
let dict = [
['版本', counter.version, ''],
['用户', users.pagination.total, '人'],
['模拟', counter.mock, '次'],
['团队', organizations.pagination.total, '个'],
['仓库', repositories.pagination.total, '个'],
['接口', interfaces.pagination.total, '个']
]
return (
<article className='Status'>
{/* <div className='header'><span className='title'>分析和报告</span></div> */}
<div className='body'>
<div className='row mb20'>
{dict.map(([name, value, unit]) =>
<div key={name} className='col-12 col-sm-6 col-md-4 col-lg-2'>
<div className='card'>
<div className='card-block'>
<div className='card-title name'>{name}</div>
<div>
<span className='value techfont'>{value}</span>
<span className='unit'>{unit}</span>
</div>
</div>
</div>
</div>
)}
</div>
<div className='row'>
<div className='col-12 col-sm-12 col-md-12 col-lg-6 chart'>
<div className='header'><span className='title'>最近 30 天新建仓库数</span></div>
<RChart type='line' data={Status.adapt(analyticsRepositoriesCreated, '新建仓库数')} options={{}} />
</div>
<div className='col-12 col-sm-12 col-md-12 col-lg-6 chart'>
<div className='header'><span className='title'>最近 30 天活跃仓库数</span></div>
<RChart type='line' data={Status.adapt(analyticsRepositoriesUpdated, '活跃仓库数')} options={{}} />
</div>
<div className='col-12 col-sm-12 col-md-12 col-lg-6 chart'>
<div className='header'><span className='title'>最近 30 天活跃用户排行</span></div>
<RChart type='horizontalBar' data={Status.adapt(analyticsUsersActivation, '操作')} options={{}} />
</div>
<div className='col-12 col-sm-12 col-md-12 col-lg-6 chart'>
<div className='header'><span className='title'>最近 30 天活跃仓库排行</span></div>
<RChart type='horizontalBar' data={Status.adapt(analyticsRepositoriesActivation, '操作')} options={{}} />
</div>
</div>
</div>
</article>
)
}
}
const mapStateToProps = (state) => ({
counter: state.counter,
users: state.users,
organizations: state.organizations,
repositories: state.repositories,
interfaces: state.interfaces,
analyticsRepositoriesCreated: state.analyticsRepositoriesCreated.data,
analyticsRepositoriesUpdated: state.analyticsRepositoriesUpdated.data,
analyticsUsersActivation: state.analyticsUsersActivation.data,
analyticsRepositoriesActivation: state.analyticsRepositoriesActivation.data
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Status)

@ -0,0 +1,24 @@
.Status
padding: 2rem;
> .header
margin-bottom: 3rem;
> .title
font-size: 2rem;
.body
text-align: center;
.card
margin-bottom: 1rem;
border-radius: .4rem;
.name
font-size: 1rem
.value
font-size: 5rem;
.unit
margin-left: 5px;
font-size: 1rem;
color: #ccc;
.chart
> .header > .title
font-size: 2rem;
margin-bottom: 1rem;
.footer

@ -0,0 +1,170 @@
import React, { Component } from 'react'
import { PropTypes, connect, Link, Mock, URI, StoreStateRouterLocationURI } from '../../family'
import { Spin, Tree } from '../utils'
import { serve } from '../../relatives/services/constant'
import './Tester.css'
class Tester extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
repository: PropTypes.object.isRequired,
location: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
requestData: {},
template: null,
schema: null,
data: null,
target: serve
}
}
render () {
let { store } = this.context
let { location: { params }, 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 每次获取仓库都显示加载动画不合理应该只在初始加载时显示动画
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 || []
let { requestData } = this.state
let requestURI = URI(`${serve}/app/mock/data/${itf.id}`)
for (let key in requestData) {
if (requestData[key] === '') requestURI.removeSearch(key)
else requestURI.setSearch(key, requestData[key])
}
let uri = StoreStateRouterLocationURI(store).removeSearch('itf')
return (
<section className='Tester'>
<div className='header'>
<div className='card-mods clearfix'>
<div className='card-title'>模块</div>
<ul className='clearfix'>
{repository.modules.map(item =>
<li key={item.id} className={item.id === mod.id ? 'active' : ''}>
<Link to={uri.setSearch('mod', item.id).href()}>{item.name}</Link>
</li>
)}
</ul>
</div>
<div className='card-itfs clearfix'>
<div className='card-title'>接口</div>
<ul className='clearfix'>
{mod.interfaces.map(item =>
<li key={item.id} className={item.id === itf.id ? 'active' : ''}>
<Link to={uri.setSearch('mod', mod.id).setSearch('itf', item.id).href()} onClick={e => this.switchItf(item)}>{item.name}</Link>
</li>
)}
</ul>
</div>
</div>
<div className='body'>
<div className='card-props clearfix'>
<form onSubmit={e => { e.preventDefault(); this.handleRequest(itf) }}>
<div className='mb6 ml6 font-bold'>业务系统</div>
<input value={this.state.target} onChange={e => this.setState({ target: e.target.value })} className='form-control' />
<ul className='fields clearfix'>
{Object.keys(requestData).map(key =>
<li key={key} className='filed'>
<div className='label'>{key}</div>
<input value={requestData[key]} onChange={e => this.updateRequestData(e, key, e.target.value)} className='form-control' />
</li>
)}
</ul>
<button className='btn btn-success' type='submit'>Submit</button>
</form>
</div>
<div className='card-result'>
<div className='card-title'>
<Link to={requestURI.href()} target='_blank'>{decodeURI(requestURI.href())}</Link>
</div>
<div>
<pre>{JSON.stringify(this.state.data, null, 2)}</pre>
</div>
</div>
</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]
// this.switchMod(undefined, mod)
// this.switchItf(undefined, itf)
}
switchMod = (mod) => {
// this.setState({ mod })
}
switchItf = (itf) => {
let requestProperties = itf.properties.map(property => ({ ...property })).filter(property => property.scope === 'request')
let requestTemplate = Tree.treeToJson(Tree.arrayToTree(requestProperties))
let requestData = Mock.mock(requestTemplate)
this.setState({ requestData }, () => {
this.handleRequest(itf)
})
}
handleRequest = (itf) => {
let requests = [
fetch(`${serve}/app/mock/template/${itf.id}`).then(res => res.json()),
fetch(`${serve}/app/mock/schema/${itf.id}`).then(res => res.json())
]
let { requestData } = this.state
let uri = URI(`${serve}/app/mock/data/${itf.id}`)
for (let key in requestData) {
if (requestData[key] === '') uri.removeSearch(key)
else uri.setSearch(key, requestData[key])
}
requests.push(
fetch(uri).then(res => res.json())
)
let { repositoryId, method, url } = itf
let target = `${this.state.target}/app/mock/${repositoryId}/${method}/${url}`
let proxy = `${serve}/proxy?target=${target}`
requests.push(
fetch(proxy).then(res => res.json())
)
Promise.all(requests).then(([template, schema, data, target]) => {
this.setState({ template, 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, target)
for (var i = 0; i < result.length; i++) {
console.warn(Assert.message(result[i]))
}
})
}
updateRequestData = (e, key, value) => {
this.setState({
requestData: { ...this.state.requestData, [key]: value }
})
}
}
//
const mapStateToProps = (state) => ({
repository: state.repository
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Tester)

@ -0,0 +1,53 @@
@import "../../assets/variables.sass";
.Tester
.header
padding: 2rem 2rem 1rem 2rem;
border-bottom: 1px solid #e1e4e8;
background-color: #fafbfc;
.card-mods, .card-itfs
display: flex;
margin-bottom: 1rem;
> .card-title
margin-bottom: 0;
width: 5rem;
min-width: 5rem;
font-size: 1.4rem;
> ul
margin-bottom: 0;
padding-left: 0;
list-style: none;
> li
float: left;
margin-right: 1rem;
padding: 2px 0;
width: 10rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
a
color: #666;
> li.active
a
color: $brand;
.body
padding: 2rem;
.card-props
margin-bottom: 2rem;
ul.fields
list-style: none;
padding-left: 0;
> li.filed
float: left;
margin-right: 1rem;
width: 10rem;
.label
margin-bottom: .5rem;
padding-left: .5rem;
font-weight: bold;
.card-result
margin-top: 1rem;
.card-title
font-size: 1.2rem;
margin-bottom: 1rem;
ul.fields

@ -0,0 +1,75 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './Dialog.css'
class Dialog extends Component {
static childContextTypes = {
dialog: PropTypes.instanceOf(Dialog)
}
constructor (props) {
super(props)
this.state = { visible: true }
}
getChildContext () {
return {
dialog: this
}
}
open = () => {
this.setState({ visible: true }, () => {
this.rePosition()
})
}
rePosition = () => {
let left = (document.documentElement.clientWidth - this.related.clientWidth) / 2
let top = (document.documentElement.clientHeight - this.related.clientHeight) / 2
this.related.style.left = left + 'px'
this.related.style.top = top + 'px'
}
handleEsc = (e) => {
if (e.keyCode === 27) {
this.close()
}
}
close = () => {
this.setState({ visible: false }, () => {
this.props.onClose()
})
}
componentDidMount () {
this.open()
document.body.classList.add('modal-open')
document.body.addEventListener('keyup', this.handleEsc)
window.addEventListener('resize', this.rePosition)
}
componentWillUnmount () {
document.body.classList.remove('modal-open')
document.body.removeEventListener('keyup', this.handleEsc)
window.removeEventListener('resize', this.rePosition)
}
render () {
if (!this.state.visible) return null
return (
<div className='dialog' ref={related => { this.related = related }}>
<button type='button' className='dialog-close' onClick={this.close}>
<span className='rapfont'>&#xe74c;</span>
</button>
<div className='dialog-content'>
{this.props.content || this.props.children}
{/*
<div className="dialog-header">
<h4 className="dialog-title">Title</h4>
</div>
<div className="dialog-body">Body</div>
<div className="dialog-footer">
<button type="button" className="btn btn-default">Close</button>
<button type="button" className="btn btn-success">Save</button>
</div>
*/}
</div>
</div>
)
}
}
export default Dialog

@ -0,0 +1,55 @@
@import "../../assets/variables.sass";
.dialog
position: fixed;
top: 0;
left: 0;
background-color: white;
border: none;
border-radius: .5rem;
box-shadow: none;
z-index: 1040;
.dialog-close
position: absolute;
right: 1rem;
top: 1rem;
border: none;
background-color: transparent;
opacity: .2;
.rapfont
font-size: 3rem;
&:hover, &:focus
color: #000;
text-decoration: none;
filter: alpha(opacity=50);
opacity: .5;
.dialog-content
.dialog-header
border-bottom: 1px solid $border;
padding: 1.5rem 4rem 1.5rem 3rem;
text-align: left;
.dialog-title
margin: 0;
font-size: 1.8rem;
.dialog-body
padding: 1.5rem 3rem;
.dialog-footer
text-align: left;
border-top: 1px solid $border;
padding: 1.5rem 3rem;
.dialog-backdrop
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #000;
filter: alpha(opacity=50);
opacity: .5;
// display: none;
z-index: 1039;

@ -0,0 +1,54 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Dialog from '../utils/Dialog'
class DialogController extends Component {
static contextTypes = {
history: PropTypes.object,
location: PropTypes.object,
match: PropTypes.object
}
static childContextTypes = {
controller: PropTypes.instanceOf(DialogController)
}
getChildContext () {
return {
controller: this
}
}
constructor (props) {
super(props)
this.state = { visible: false }
}
open = () => {
this.setState({ visible: true })
}
close = () => {
this.setState({ visible: false })
}
resolve = () => {
let { onResolved } = this.props
if (onResolved) {
onResolved()
return
}
console.warn('警告Dialog 的回调应该放到调用组件中!')
let { history, location } = this.context
history.push(location.pathname + location.search + location.hash)
}
render () {
let dialog
if (this.state.visible) {
dialog = <Dialog onClose={this.close} content={this.props.content} />
}
return (
<span>
<span onClick={this.open}>{this.props.children}</span>
{this.state.visible ? dialog : null}
{this.state.visible ? <div className='dialog-backdrop' /> : null}
</span>
)
}
}
export default DialogController

@ -0,0 +1,25 @@
import React, { Component } from 'react'
// TODO 2.x Form
class Form extends Component {
render () {
let { children } = this.props
return (
<form onSubmit={this.handleSubmit}>
{children}
</form>
)
}
handleSubmit = (event) => {
event.preventDefault()
let { onSubmit, onResolved, onRejected } = this.props
var values = {};
([]).slice.call(event.target.elements, 0).forEach(el => {
if (el.name) values[el.name] = el.value
})
onSubmit(values)
onResolved()
onRejected()
}
}
export default Form

@ -0,0 +1,82 @@
import React, { Component } from 'react'
import _ from 'lodash'
import AccountService from '../../relatives/services/Account'
import './TagsInput.css'
import { GoX } from 'react-icons/lib/go'
// TODO 2.3 input
// TODO 2.3 退 退
class MembersInput extends Component {
constructor (props) {
super(props)
this.state = {
seed: '',
value: props.value || [], // [{ id, fullname, email }]
options: [], // [{ id, fullname, email }]
limit: props.limit
}
}
componentWillReceiveProps (nextProps) {
this.setState({
value: nextProps.value || []
})
}
render () {
return (
<div className='TagsInput clearfix' onClick={e => this.$seed && this.$seed.focus()}>
{this.state.value.map(item =>
<span key={item.id} className='tag'>
<span className='label'>{item.fullname}</span>
<span className='remove' onClick={e => this.handleRemove(item)}><GoX /></span>
</span>
)}
{(!this.state.limit || this.state.value.length < this.state.limit) &&
<div className='dropdown'>
<input className='dropdown-input' value={this.state.seed} placeholder='名字检索' autoComplete='off'
onChange={e => this.handleSeed(e.target.value)}
ref={$seed => { this.$seed = $seed }} />
{this.state.options.length ? (
<div className='dropdown-menu' ref={$optons => { this.$optons = $optons }}>
{this.state.options.map(item =>
<a key={item.id} href='' className='dropdown-item'
onClick={e => this.handleSelect(e, item)}>{item.fullname}</a>
)}
</div>
) : null}
</div>
}
</div>
)
}
handleSeed = async (seed) => {
this.setState({ seed: seed })
if (!seed) {
this.setState({ options: [] })
return
}
let users = await AccountService.fetchUserList({ name: seed })
let options = _.differenceWith(users.data, this.state.value, _.isEqual)
this.setState({ options })
}
handleSelect = (e, selected) => {
e.preventDefault()
let nextState = { seed: '', value: [...this.state.value, selected] }
this.setState(nextState, this.handleChange)
}
// remove vs delete
handleRemove = (removed) => {
let nextState = { value: this.state.value.filter(item => item !== removed) }
this.setState(nextState, this.handleChange)
}
// change vs update
handleChange = () => {
let { onChange } = this.props
onChange(this.state.value)
this.$seed && this.$seed.focus()
this.setState({
options: []
})
}
}
export default MembersInput

@ -0,0 +1,105 @@
// DONE 2.x context react-modal
import React, { Component } from 'react'
import { PropTypes, render, Provider } from '../../family'
import './Modal.css'
/*
<div className='modal-header'>
<h4 className='modal-title'>Title</h4>
</div>
<div className='modal-body'>Body</div>
<div className='modal-footer'>
<button type='button' className='btn btn-default'>Close</button>
<button type='button' className='btn btn-success'>Save</button>
</div>
*/
class Modal extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {}
static childContextTypes = {}
render () {
return null
}
$modal: null
$backdrop: null
componentDidMount () {
document.body.classList.add('modal-open')
document.body.addEventListener('keyup', this.handleEsc)
window.addEventListener('resize', this.rePosition)
this.withBackdrop()
this.withPortal(() => {
this.rePosition()
})
}
componentWillUnmount () {
document.body.classList.remove('modal-open')
document.body.removeEventListener('keyup', this.handleEsc)
window.removeEventListener('resize', this.rePosition)
document.body.removeChild(this.$modal)
document.body.removeChild(this.$backdrop)
}
withPortal (callback) {
if (!this.$modal) {
let $modal = document.createElement('div')
$modal.className = 'modal-portal'
document.body.appendChild($modal)
this.$modal = $modal
}
let dialog = (
<Provider store={this.context.store}>
<div className='Modal' ref={$dialog => { this.$dialog = $dialog }}>
{this.props.children}
<button type='button' className='close' onClick={e => this.props.onClose()}>
<span className='rapfont'>&#xe74c;</span>
</button>
</div>
</Provider>
)
render(dialog, this.$modal, callback)
}
withBackdrop () {
if (!this.$backdrop) {
let $backdrop = document.createElement('div')
$backdrop.className = 'modal-backdrop'
document.body.appendChild($backdrop)
this.$backdrop = $backdrop
}
}
static rePosition ($dialog) {
let left = (document.documentElement.clientWidth - $dialog.clientWidth) / 2
let top = (document.documentElement.clientHeight - $dialog.clientHeight) / 2
$dialog.style.left = left + 'px'
$dialog.style.top = top + 'px'
}
rePosition = () => {
Modal.rePosition(this.$dialog)
}
handleEsc = (e) => {
if (e.keyCode === 27) this.props.onClose()
}
// if (!this.state.visible) {
// if ($backdrop) document.body.removeChild($backdrop)
// return
// }
// open = (e) => {
// if (e) e.preventDefault()
// this.setState({ visible: true }, () => {
// this.withBackdrop()
// this.withPortal(() => {
// this.rePosition()
// })
// })
// }
// close = () => {
// this.setState({ visible: false }, () => {
// this.withBackdrop()
// })
// }
}
export default Modal

@ -0,0 +1,51 @@
@import "../../assets/variables.sass";
.Modal
position: fixed;
top: 0;
left: 0;
max-height: 100%;
overflow: scroll;
border: none;
border-radius: .5rem;
box-shadow: none;
background-color: white;
z-index: 1040;
button.close
position: absolute;
right: 1rem;
top: 1rem;
border: none;
color: #000;
background-color: transparent;
opacity: .2;
&:hover, &:focus
opacity: .5;
text-decoration: none;
.rapfont
font-size: 3rem;
.modal-header
border-bottom: 1px solid $border;
padding: 1.5rem 4rem 1.5rem 3rem;
text-align: left;
.modal-title
margin: 0;
font-size: 1.8rem;
.modal-body
padding: 1.5rem 3rem;
.modal-footer
text-align: left;
border-top: 1px solid $border;
padding: 1.5rem 3rem;
.modal-backdrop
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #000;
filter: alpha(opacity=50);
opacity: .5;
z-index: 1039;

@ -0,0 +1,54 @@
import React, { Component } from 'react'
import { PropTypes, connect } from '../../family'
import Modal from './Modal'
import './Utils.css'
class ModalContent extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
static propTypes = {
auth: PropTypes.object.isRequired
}
render () {
return (
<div style={{ width: '400px', height: '400px' }}>
{JSON.stringify(this.props.auth)}
</div>
)
}
}
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = ({})
let ModalContentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ModalContent)
class ModalExample extends Component {
static contextTypes = {}
constructor (props) {
super(props)
this.state = { visible: false }
}
render () {
let visible = this.state.visible
return (
<div>
<button onClick={e => this.setState({ visible: !visible })}>Trigger</button>
{visible &&
<Modal onClose={e => this.setState({ visible: false })}>
<ModalContentContainer />
<ModalContentContainer />
<ModalContentContainer />
</Modal>
}
</div>
)
}
}
export default ModalExample

@ -0,0 +1,7 @@
import React from 'react'
export default ({ location }) => (
<div className='p100 fontsize-16 text-center'>
<span>No match for <code>{location.pathname}</code></span>
</div>
)

@ -0,0 +1,235 @@
import React from 'react'
import { Link } from 'react-router-dom'
import URI from 'urijs'
import _ from 'lodash'
import './Pagination.css'
export const Pagination = ({ location, url, calculated, total, cursor = 1, limit = 10, className }) => {
if (location && !url) url = location.pathname + location.hash + location.search
if (calculated) ({ total, cursor, limit } = calculated)
if (!total) return null
let paging = new Calculator(total, cursor, limit)
url = URI(url).setSearch({ limit: paging.limit })
let step = 7
let barStart = Math.min(
paging.pages,
Math.max(
1,
paging.cursor - parseInt(step / 2, 10)
)
)
let barEnd = Math.min(paging.pages, barStart + step - 1)
return (
<nav className='Pagination clearfix'>
<div className='summary'>
<span>{`当前第 ${paging.start + 1} - ${paging.end} 条,共 ${paging.total} 条,每页展现 ${paging.limit}`}</span>
</div>
<ul className='page-list'>
<li className={paging.hasPrev ? 'page-item' : 'page-item-disabled'}>
<Link className='page-link' to={URI(url).setSearch({ cursor: paging.prev }).href()}><span>&laquo;</span></Link>
</li>
{barStart === 2 &&
<li className='page-item'>
<Link className='page-link' to={URI(url).setSearch({ cursor: 1 }).href()}>1</Link>
</li>
}
{barStart >= 3 &&
<li className='page-item'>
<Link className='page-link' to={URI(url).setSearch({ cursor: 1 }).href()}>1</Link>
</li>
}
{barStart >= 3 &&
<li className='page-item'>
<Link className='page-link' to={URI(url).setSearch({ cursor: 2 }).href()}>2</Link>
</li>
}
{barStart > 3 &&
<li className='page-item-disabled'>
<Link className='page-link' to=''>...</Link>
</li>
}
{_.range(barStart, barEnd + 1).map(page =>
<li key={page} className={page === +cursor ? 'page-item-active' : 'page-item'}>
<Link className='page-link' to={URI(url).setSearch({ cursor: page }).href()}>{page}</Link>
</li>
)}
{barEnd < paging.pages - 1 &&
<li className='page-item-disabled'>
<Link className='page-link' to=''>...</Link>
</li>
}
{barEnd < paging.pages &&
<li className='page-item'>
<Link className='page-link' to={URI(url).setSearch({ cursor: paging.pages }).href()}>{paging.pages}</Link>
</li>
}
<li className={paging.hasNext ? 'page-item' : 'page-item-disabled'}>
<Link className='page-link' to={URI(url).setSearch({ cursor: paging.next }).href()}><span>&raquo;</span></Link>
</li>
</ul>
</nav>
)
}
export default Pagination
/*
Pagination
Pure paging implementation reference.
纯粹的分页参考实现
属性
data 数据
total 总条数
cursor 当前页数第几页 1 开始计算
limit 分页大小
pages 总页数
start 当前页的起始下标
end 当前页的结束下标
hasPrev 是否有前一页
hasNext 是否有下一页
hasFirst 是否有第一页
hasLast 是否有最后一页
prev 前一页
next 后一页
first 第一页
last 最后一页
focus 当前页的当前焦点下标
方法
calc() 计算分页状态当属性值发生变化时方法 calc() 被调用
moveTo(cursor) 移动到指定页
moveToPrev() 移动到前一页
moveToNext() 移动到下一页
moveToFirst() 移动到第一页
moveToLast() 移动到最后一页
fetch(arr) 获取当前页的数据或者用当前状态获取参数 arr 的子集
setData(data) 更新数据集合
setTotal(total) 更新总条数
setCursor(cursor) 更新当前页数
setFocus(focus) 设置当前焦点
setLimit(limit) 设置分页大小
get(focus) 获取一条数据
toString() 友好打印
toHTML(url) 生成分页栏
*/
/*
new State( data, cursor, limit )
new State( total, cursor, limit )
*/
export function Calculator (data, cursor, limit) {
this.data = (typeof data === 'number' || typeof data === 'string') ? undefined : data
this.total = this.data ? this.data.length : parseInt(data, 10)
this.cursor = parseInt(cursor, 10)
this.limit = parseInt(limit, 10)
this.calc()
}
Calculator.prototype = {
calc: function () {
if (this.total && parseInt(this.total, 10) > 0) {
this.limit = this.limit < 1 ? 1 : this.limit
this.pages = (this.total % this.limit === 0) ? this.total / this.limit : this.total / this.limit + 1
this.pages = parseInt(this.pages, 10)
this.cursor = (this.cursor > this.pages) ? this.pages : this.cursor
this.cursor = (this.cursor < 1) ? this.pages > 0 ? 1 : 0 : this.cursor
this.start = (this.cursor - 1) * this.limit
this.start = (this.start < 0) ? 0 : this.start // 0
this.end = (this.start + this.limit > this.total) ? this.total : this.start + this.limit
this.end = (this.total < this.limit) ? this.total : this.end
this.hasPrev = (this.cursor > 1)
this.hasNext = (this.cursor < this.pages)
this.hasFirst = this.hasPrev
this.hasLast = this.hasNext
this.prev = this.hasPrev ? this.cursor - 1 : 0
this.next = this.hasNext ? this.cursor + 1 : 0
this.first = this.hasFirst ? 1 : 0
this.last = this.hasLast ? this.pages : 0
this.focus = this.focus ? this.focus : 0
this.focus = this.focus % this.limit + this.start
this.focus = this.focus > this.end - 1 ? this.end - 1 : this.focus
} else {
this.pages = this.cursor = this.start = this.end = 0
this.hasPrev = this.hasNext = this.hasFirst = this.hasLast = false
this.prev = this.next = this.first = this.last = 0
this.focus = 0
}
return this
},
moveTo: function (cursor) {
this.cursor = parseInt(cursor, 10)
return this.calc()
},
moveToPrev: function () {
return this.moveTo(this.cursor - 1)
},
moveToNext: function () {
return this.moveTo(this.cursor + 1)
},
moveToFirst: function () {
return this.moveTo(1)
},
moveToLast: function () {
return this.moveTo(this.pages)
},
fetch: function (arr) {
return (arr || this.data).slice(this.start, this.end)
},
setData: function (data) {
this.data = data
this.total = data.length
return this.calc()
},
setTotal: function (total) {
this.total = parseInt(total, 10)
return this.calc()
},
setCursor: function (cursor) {
this.cursor = parseInt(cursor, 10)
return this.calc()
},
setFocus: function (focus) {
this.focus = parseInt(focus, 10)
if (this.focus < 0) this.focus += this.total
if (this.focus >= this.total) this.focus -= this.total
this.cursor = parseInt(this.focus / this.limit, 10) + 1
return this.calc()
},
setLimit: function (limit) {
this.limit = parseInt(limit, 10)
return this.calc()
},
get: function (focus) {
if (focus !== undefined) return this.data[focus % this.data.length]
else return this.data[this.focus]
},
toString: function () {
return JSON.stringify(this, null, 4)
},
map: function (iteratee) {
let result = []
for (let i = 0; i < this.pages; i++) {
result.push(iteratee(i + 1))
}
return result
}
}
Calculator.prototype.to = Calculator.prototype.moveTo
Calculator.prototype.toPrev = Calculator.prototype.moveToPrev
Calculator.prototype.toNext = Calculator.prototype.moveToNext
Calculator.prototype.toFirst = Calculator.prototype.moveToFirst
Calculator.prototype.toLast = Calculator.prototype.moveToLast

@ -0,0 +1,33 @@
@import "../../assets/variables.sass";
.Pagination
.summary
float: left;
padding: 0.5rem 0;
.page-list
float: right;
display: flex;
margin-bottom: 0;
padding-left: 0;
list-style: none;
.page-item
margin: 0 .2rem;
.page-link
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 0.4rem;
&:hover
color: white;
background-color: $brand-hover;
.page-item-disabled
@extend .page-item;
.page-link
color: #ccc;
pointer-events: none;
cursor: not-allowed;
.page-item-active
@extend .page-item;
.page-link
color: white;
background-color: $brand;

@ -0,0 +1,127 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import './Popover.css'
// jQuery .offset()
const [TOP, BOTTOM, LEFT, RIGHT, CENTER] = ['top', 'bottom', 'left', 'right', 'center']
class Popover extends Component {
constructor (props) {
super(props)
this.state = {
visible: false,
placement: props.placement || BOTTOM,
align: props.align || CENTER,
width: props.width
// title: this.props.title,
// content: this.props.content
}
}
show = () => {
clearTimeout(this.timer)
this.setState({ visible: true }, () => {
this.reposition()
})
}
hide = () => {
this.timer = setTimeout(() => {
this.setState({ visible: false })
}, 100)
}
fixRect (rect) {
let html = document.documentElement
let top = rect.top + window.pageYOffset - html.clientTop
let left = rect.left + window.pageXOffset - html.clientLeft
return {
top,
right: left + rect.width,
bottom: top + rect.height,
left,
width: rect.width,
height: rect.height
}
}
replacement (elem, related) {
let elemRect = this.fixRect(elem.getBoundingClientRect())
let relatedRect = this.fixRect(related.getBoundingClientRect())
let top, left
let diff = {
top: elemRect.height / 2 - relatedRect.height / 2,
left: elemRect.width / 2 - relatedRect.width / 2
}
switch (this.state.placement) {
case TOP: //
top = elemRect.top - relatedRect.height
left = elemRect.left + diff.left
break
case BOTTOM: //
top = elem.offsetHeight // elemRect.top + elem.offsetHeight
left = diff.left // elemRect.left + diff.left
break
case LEFT: //
left = elemRect.left - relatedRect.width
top = elemRect.top + diff.top
break
case RIGHT: //
left = elemRect.left + elemRect.width
top = elemRect.top + diff.top
break
default:
}
return {top, left}
}
realign (elem, related, offset) {
let elemRect = this.fixRect(elem.getBoundingClientRect())
let relatedRect = this.fixRect(related.getBoundingClientRect())
switch (this.state.align) {
case TOP: //
offset.top = 0 // elemRect.top
break
case BOTTOM: //
offset.top = elemRect.height // elemRect.bottom - relatedRect.height
break
case LEFT: //
offset.left = 0 // elemRect.let
break
case RIGHT: //
offset.left = elemRect.width - relatedRect.width // elemRect.right - relatedRect.width
break
case CENTER: //
break
default:
}
return offset
}
reposition () {
let elem = ReactDOM.findDOMNode(this)
let related = this.related
let offset = this.replacement(elem, related)
offset = this.realign(elem, related, offset)
offset.top += parseFloat(window.getComputedStyle(related).marginTop)
offset.left += parseFloat(window.getComputedStyle(related).marginLeft)
related.style.top = offset.top + 'px'
related.style.left = offset.left + 'px'
}
componentDidMount () {
if (this.state.visible) this.reposition()
}
render () {
let popover
if (this.state.visible) {
popover = (
<div className={`popover ${this.state.placement}`} style={{ width: this.props.width }}ref={related => { this.related = related }}>
<div className='arrow' />
<div className='popover-title'>{this.props.title}</div>
<div className='popover-content'>{this.props.content}</div>
</div>
)
}
return (
<span className={`Popover ${this.props.className || ''}`} onMouseOver={this.show} onMouseOut={this.hide}>
{this.props.children}
{popover}
</span>
)
}
}
export default Popover

@ -0,0 +1,40 @@
@import "../../assets/variables.sass";
.Popover
position: relative;
.popover
position: absolute;
top: 0;
left: 0;
z-index: 1060;
max-width: 30rem;
display: block;
padding: 0;
border: 1px solid $border;
border-radius: 4px;
box-shadow: 0 6px 8px rgba(51, 51, 51, 0.08);
font-family: $font-family;
background-color: white;
&.top { margin-top: -10px; }
&.right { margin-left: 10px; }
&.bottom { margin-top: 10px; }
&.left { margin-left: -10px; }
.arrow
display: none;
.popover-title
margin: 0;
padding: 0.5rem 1rem; // 8px 14px
border-bottom: 1px solid #ebebeb;
border-radius: .4rem .4px 0 0;
font-size: 14px;
background-color: #fafafa;
.popover-content
margin: 0;
padding: 0.5rem 1rem; // 8px 14px
font-size: 12px;
line-height: 22px;

@ -0,0 +1,10 @@
import React from 'react'
import Popover from './Popover'
export default () => (
<div>
{['top', 'right', 'bottom', 'left'].map(placement =>
<Popover key={placement} placement={placement} title={placement} content='Envy is the ulcer of the soul.' className='btn btn-default mr10'>Popover on {placement}</Popover>
)}
</div>
)

@ -0,0 +1,29 @@
import React, { Component } from 'react'
import { render } from 'react-dom'
// TODO 2.x http://stackoverflow.com/questions/28802179/how-to-create-a-react-modalwhich-is-append-to-body-with-transitions
let PORTAL_ID = 1
class Portal extends Component {
render () { return null }
$portal: null
componentDidMount () {
let id = 'Portal-' + PORTAL_ID++
let $portal = document.getElementById(id)
if (!$portal) {
$portal = document.createElement('div')
$portal.id = id
document.body.appendChild($portal)
}
this.$portal = $portal
this.componentDidUpdate()
}
componentWillUnmount () {
document.body.removeChild(this.$portal)
}
componentDidUpdate () {
render(<div {...this.props}>{this.props.children}</div>, this.$portal)
}
}
export default Portal

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

Loading…
Cancel
Save