- 移除 bootstrap
- material 重构
- 嵌套数据折叠功能
test
bigfengyu 5 years ago
parent 13de5b33e8
commit 78bbfb2858

@ -21,7 +21,6 @@ export interface RootState {
}
export interface Organization {
id: number
name: string
@ -44,7 +43,6 @@ export interface Organization {
owner?: User
newOwner?: User
}
export interface User {
@ -65,3 +63,79 @@ export interface IConfig {
key: string
}
}
export interface Repository {
id: number
name: string
description?: string
logo?: string
/** true: 公开, false: 私有 */
visibility?: boolean
creatorId?: number
ownerId?: number
organizationid?: number
memberIds?: number[]
members?: User[]
owner?: User
newOwner?: User
collaborators?: Repository[]
collaboratorIds?: string[]
collaboratorIdstring?: string
}
export interface Module {
id: number
name: string
description?: string
repositoryid?: number
creatorId?: number
priority: number
repository?: Repository
repositoryId?: number
}
export interface Interface {
id: number
name: string
url: string
method: string
description?: string
moduleId?: number
creatorId?: number
lockerid?: number
locker?: User
repositoryId?: number
repository?: Repository
status?: number
}

@ -152,4 +152,199 @@ table.table
padding: 1.25rem;
button:focus
outline: none;
outline: none;
.dropup, .dropright, .dropdown, .dropleft
position: relative
.dropdown-toggle
white-space: nowrap
&::after
display: inline-block
margin-left: 0.255em
vertical-align: 0.255em
content: ""
border-top: 0.3em solid
border-right: 0.3em solid transparent
border-bottom: 0
border-left: 0.3em solid transparent
&:empty::after
margin-left: 0
.dropdown-menu
position: absolute
top: 100%
left: 0
z-index: 1000
display: none
float: left
min-width: 10rem
padding: 0.5rem 0
margin: 0.125rem 0 0
font-size: 1rem
color: #212529
text-align: left
list-style: none
background-color: #fff
background-clip: padding-box
border: 1px solid rgba(0, 0, 0, 0.15)
border-radius: 0.25rem
.dropdown-menu-left
right: auto
left: 0
.dropdown-menu-right
right: 0
left: auto
@media (min-width: 576px)
.dropdown-menu-sm-left
right: auto
left: 0
.dropdown-menu-sm-right
right: 0
left: auto
@media (min-width: 768px)
.dropdown-menu-md-left
right: auto
left: 0
.dropdown-menu-md-right
right: 0
left: auto
@media (min-width: 992px)
.dropdown-menu-lg-left
right: auto
left: 0
.dropdown-menu-lg-right
right: 0
left: auto
@media (min-width: 1200px)
.dropdown-menu-xl-left
right: auto
left: 0
.dropdown-menu-xl-right
right: 0
left: auto
.dropup
.dropdown-menu
top: auto
bottom: 100%
margin-top: 0
margin-bottom: 0.125rem
.dropdown-toggle
&::after
display: inline-block
margin-left: 0.255em
vertical-align: 0.255em
content: ""
border-top: 0
border-right: 0.3em solid transparent
border-bottom: 0.3em solid
border-left: 0.3em solid transparent
&:empty::after
margin-left: 0
.dropright
.dropdown-menu
top: 0
right: auto
left: 100%
margin-top: 0
margin-left: 0.125rem
.dropdown-toggle
&::after
display: inline-block
margin-left: 0.255em
vertical-align: 0.255em
content: ""
border-top: 0.3em solid transparent
border-right: 0
border-bottom: 0.3em solid transparent
border-left: 0.3em solid
&:empty::after
margin-left: 0
&::after
vertical-align: 0
.dropleft
.dropdown-menu
top: 0
right: 100%
left: auto
margin-top: 0
margin-right: 0.125rem
.dropdown-toggle
&::after
display: inline-block
margin-left: 0.255em
vertical-align: 0.255em
content: ""
display: none
&::before
display: inline-block
margin-right: 0.255em
vertical-align: 0.255em
content: ""
border-top: 0.3em solid transparent
border-right: 0.3em solid
border-bottom: 0.3em solid transparent
&:empty::after
margin-left: 0
&::before
vertical-align: 0
.dropdown-menu
&[x-placement^="top"], &[x-placement^="right"], &[x-placement^="bottom"], &[x-placement^="left"]
right: auto
bottom: auto
.dropdown-divider
height: 0
margin: 0.5rem 0
overflow: hidden
border-top: 1px solid #e9ecef
.dropdown-item
display: block
width: 100%
padding: 0.25rem 1.5rem
clear: both
font-weight: 400
color: #212529
text-align: inherit
white-space: nowrap
background-color: transparent
border: 0
&:hover, &:focus
color: #16181b
text-decoration: none
background-color: #f8f9fa
&.active, &:active
color: #fff
text-decoration: none
background-color: #007bff
&.disabled, &:disabled
color: #6c757d
pointer-events: none
background-color: transparent
.dropdown-menu.show
display: block
.dropdown-header
display: block
padding: 0.5rem 1.5rem
margin-bottom: 0
font-size: 0.875rem
color: #6c757d
white-space: nowrap
.dropdown-item-text
display: block
padding: 0.25rem 1.5rem
color: #212529

@ -8,81 +8,192 @@
@import "./shortcuts.sass"
html
font-size: 62.5%; // 10 ÷ 16 × 100% = 62.5%
font-size: 62.5% // 10 ÷ 16 × 100% = 62.5%
html
font-family: sans-serif
line-height: 1.15
-webkit-text-size-adjust: 100%
-webkit-tap-highlight-color: transparent
*, :after, :before
box-sizing: border-box
.clearfix:after
display: block
clear: both
content: ""
img, svg
vertical-align: middle
body
font-size: 1.2rem;
line-height: 1.5;
font-family: $font-family;
-webkit-font-smoothing: antialiased;
font-size: 1.2rem
line-height: 1.5
font-family: $font-family
-webkit-font-smoothing: antialiased
margin: 0
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji
font-weight: 400
line-height: 1.5
color: #212529
text-align: left
background-color: #fff
html, body
height: 100%;
height: 100%
#root
height: 100%;
height: 100%
> .Routes
display: flex;
flex-direction: column;
min-height: 100%;
display: flex
flex-direction: column
min-height: 100%
> .body
flex-grow: 1;
display: flex;
flex-direction: column;
flex-grow: 1
display: flex
flex-direction: column
> .Spin
align-items: center;
justify-content: center;
align-items: center
justify-content: center
> * //
flex-grow: 1;
display: flex;
flex-direction: column;
flex-grow: 1
display: flex
flex-direction: column
> .body
flex-grow: 1;
flex-grow: 1
button, input, optgroup, select, textarea
font-size: 1.2rem;
font-family: $font-family;
-webkit-font-smoothing: antialiased;
font-size: 1.2rem
font-family: $font-family
-webkit-font-smoothing: antialiased
// *
// transition: color .15s ease-out, background-color .15s ease-out, opacity .15s ease-out;
// transition: color .15s ease-out, background-color .15s ease-out, opacity .15s ease-out
a
&, &:hover, &:focus, &:active, &:visited
outline: 0;
text-decoration: none;
color: $brand
outline: 0
text-decoration: none
&[disabled],
&.disabled
pointer-events: none;
cursor: not-allowed;
pointer-events: none
cursor: not-allowed
a.text-decoration
&:hover
border-bottom: 1px solid $brand;
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;
margin: 0
padding: 0.5rem 0.75rem
border: 1px solid $border
border-radius: 0.4rem
background-color: $bg
white-space: pre-wrap
.btn-top
position: fixed;
right: 150px;
bottom: -1px;
width: 90px;
height: 50px;
text-align: center;
vertical-align: middle;
padding: 16px;
z-index: 99999;
background-color: #ffffff;
border: 1px solid #DDDDDD;
border-radius: 16px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
position: fixed
right: 150px
bottom: -1px
width: 90px
height: 50px
text-align: center
vertical-align: middle
padding: 16px
z-index: 99999
background-color: #ffffff
border: 1px solid #DDDDDD
border-radius: 16px
border-bottom-left-radius: 0
border-bottom-right-radius: 0
.mr8
margin-right: 8px;
margin-right: 8px
.row
display: flex
flex-wrap: wrap
margin-right: -15px
margin-left: -15px
.card
position: relative
display: flex
flex-direction: column
min-width: 0
word-wrap: break-word
background-color: #fff
background-clip: border-box
border: 1px solid rgba(0, 0, 0, 0.125)
border-radius: .25rem
.card-header
padding: .75rem 1.25rem
margin-bottom: 0
background-color: rgba(0,0,0,.03)
border-bottom: 1px solid rgba(0,0,0,.125)
.distribute-2-3
flex-basis: 66.666666%
max-width: 66.666666%
.distribute-1-3
flex-basis: 33.333333%
max-width: 33.333333%
.badge-secondary
color: #fff
background-color: #6c757d
.badge
display: inline-block
padding: .25em .4em
font-size: 75%
font-weight: 700
line-height: 1
text-align: center
white-space: nowrap
vertical-align: baseline
border-radius: .25rem
-webkit-transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out
code
font-size: 87.5%
color: #e83e8c
word-break: break-word
.alert
position: relative
padding: .75rem 1.25rem
margin-bottom: 1rem
border: 1px solid transparent
border-radius: .25rem
margin-bottom: 0
.title
margin-right: 1rem
.icon
font-size: 1.4rem
margin-right: .5rem
.msg
font-weight: bold
margin-right: 1rem
.itf
a
margin-right: 1rem
&.alert-warning
color: #856404
background-color: #fff3cd
border-color: #ffeeba
&.alert-danger
color: #721c24
background-color: #f8d7da
border-color: #f5c6cb
&.alert-info
color: #0c5460
background-color: #d1ecf1
border-color: #bee5eb

@ -7,6 +7,9 @@
.hide
display: none !important;
.float-right
float: right;
// ----------------------------------------
//
// ----------------------------------------

@ -1,7 +1,7 @@
import React from 'react'
import { serve } from '../../relatives/services/constant'
import './API.css'
import { Paper } from '@material-ui/core'
import { Paper, Button } from '@material-ui/core'
const ExampleJQuery = () => (
<div>
@ -82,8 +82,9 @@ class API extends React.Component<Props, State> {
<ul>
<li><span className="label"></span><code>{serve}/app/plugin/:repositories</code></li>
<li><span className="label">jQuery </span><code>{serve}/libs/jquery.rap.js</code>
<button
className="btn btn-secondary btn-sm ml8"
<Button
size="small"
variant="outlined"
onClick={
e => {
e.preventDefault()
@ -94,7 +95,7 @@ class API extends React.Component<Props, State> {
}
>
</button>
</Button>
</li>
{this.state.showExampleJQuery && <ExampleJQuery />}
<li><span className="label">Mock.js </span><code>{serve}/libs/mock.rap.js</code></li>

@ -154,7 +154,7 @@ const Message = withStyles(styles)(
horizontal: 'left',
}}
open={this.state.open}
autoHideDuration={60000}
autoHideDuration={2000}
onExited={this.handleExited}
onClose={this.handleClose}
>

@ -53,7 +53,7 @@ class DuplicatedInterfacesWarning extends Component<DuplicatedInterfacesWarningP
return (
<div className="DuplicatedInterfacesWarning">
{duplicated.map((interfaces, index) => (
<div key={index} className="alert alert-warning mb6">
<div key={index} className="alert alert-warning">
<span className="title">
<GoAlert className="icon" />
<span className="msg"></span>

@ -1,9 +1,12 @@
import React, { Component } from 'react'
import { PropTypes, connect, _ } from '../../family'
import InterfaceEditorToolbar from './InterfaceEditorToolbar'
import InterfaceSummary, { BODY_OPTION, REQUEST_PARAMS_TYPE, rptFromStr2Num } from './InterfaceSummary'
import InterfaceSummary, {
BODY_OPTION,
REQUEST_PARAMS_TYPE,
rptFromStr2Num
} from './InterfaceSummary'
import PropertyList from './PropertyList'
import { RModal } from '../utils'
import MoveInterfaceForm from './MoveInterfaceForm'
import { fetchRepository } from '../../actions/repository'
import { RootState } from 'actions/types'
@ -12,36 +15,38 @@ import { updateProperties } from 'actions/property'
import { updateInterface } from 'actions/interface'
export const RequestPropertyList = (props: any) => {
return <PropertyList scope="request" title="请求参数" label="请求" {...props} />
return (
<PropertyList scope="request" title="请求参数" label="请求" {...props} />
)
}
export const ResponsePropertyList = (props: any) => (
<PropertyList scope="response" title="响应内容" label="响应" {...props} />
)
type InterfaceEditorProps = {
auth: any
itf: any
properties: any[]
mod: any
repository: any
lockInterface: typeof lockInterface
unlockInterface: typeof unlockInterface
updateInterface: typeof updateInterface
updateProperties: typeof updateProperties
auth: any;
itf: any;
properties: any[];
mod: any;
repository: any;
lockInterface: typeof lockInterface;
unlockInterface: typeof unlockInterface;
updateInterface: typeof updateInterface;
updateProperties: typeof updateProperties;
}
type InterfaceEditorState = {
summaryState: any
itf: any
properties: any
editable: boolean
moveInterfaceDialogOpen: boolean,
summaryState: any;
itf: any;
properties: any;
editable: boolean;
moveInterfaceDialogOpen: boolean;
}
// TODO 2.x 参考 MySQL Workbench 的字段编辑器
// TODO 2.x 支持复制整个接口到其他模块、其他项目
class InterfaceEditor extends Component<
InterfaceEditorProps,
InterfaceEditorState
> {
> {
static childContextTypes = {
handleLockInterface: PropTypes.func.isRequired,
handleUnlockInterface: PropTypes.func.isRequired,
@ -71,7 +76,7 @@ class InterfaceEditor extends Component<
...prevStates,
itf,
properties: properties.map((property: any) => ({ ...property })),
editable: !!(itf.locker && (itf.locker.id === auth.id)),
editable: !!(itf.locker && itf.locker.id === auth.id),
}
}
getChildContext() {
@ -86,7 +91,9 @@ class InterfaceEditor extends Component<
if (
nextProps.itf.id === this.state.itf.id &&
nextProps.itf.updatedAt === this.state.itf.updatedAt
) { return }
) {
return
}
const prevStates = this.state
this.setState(InterfaceEditor.mapPropsToState(nextProps, prevStates))
}
@ -98,7 +105,9 @@ class InterfaceEditor extends Component<
const { auth, repository, mod } = this.props
const { editable, itf } = this.state
const { id, locker } = this.state.itf
if (!id) { return null }
if (!id) {
return null
}
return (
<article className="InterfaceEditor">
<InterfaceEditorToolbar
@ -110,7 +119,9 @@ class InterfaceEditor extends Component<
moveInterface={this.handleMoveInterface}
handleLockInterface={this.handleLockInterface}
handleMoveInterface={this.handleMoveInterface}
handleSaveInterfaceAndProperties={this.handleSaveInterfaceAndProperties}
handleSaveInterfaceAndProperties={
this.handleSaveInterfaceAndProperties
}
handleUnlockInterface={this.handleUnlockInterface}
/>
<InterfaceSummary
@ -144,33 +155,41 @@ class InterfaceEditor extends Component<
handleChangeProperty={this.handleChangeProperty}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
/>
<RModal
when={this.state.moveInterfaceDialogOpen}
{this.state.moveInterfaceDialogOpen && <MoveInterfaceForm
title="移动/复制接口"
mod={mod}
repository={repository}
itfId={itf.id}
open={this.state.moveInterfaceDialogOpen}
onClose={() => this.setState({ moveInterfaceDialogOpen: false })}
onResolve={this.handleMoveInterfaceSubmit}
>
<MoveInterfaceForm title="移动接口" repository={repository} itfId={itf.id} />
</RModal>
/>}
</article>
)
}
handleAddMemoryProperty = (property: any, cb: any) => {
this.handleAddMemoryProperties([property], cb)
}
};
handleAddMemoryProperties = (properties: any, cb: any) => {
const requestParamsType = this.state.summaryState.requestParamsType
const rpt = rptFromStr2Num(requestParamsType)
properties.forEach((item: any) => {
if (item.memory === undefined) { item.memory = true }
if (item.id === undefined) { item.id = _.uniqueId('memory-') }
if (item.memory === undefined) {
item.memory = true
}
if (item.id === undefined) {
item.id = _.uniqueId('memory-')
}
item.pos = rpt
})
const nextState = { properties: [...this.state.properties, ...properties] }
this.setState(nextState, () => {
if (cb) { cb(properties) }
if (cb) {
cb(properties)
}
})
}
};
handleDeleteMemoryProperty = (property: any, cb: any) => {
const properties = [...this.state.properties]
const index = properties.findIndex(item => item.id === property.id)
@ -188,10 +207,12 @@ class InterfaceEditor extends Component<
}
this.setState({ properties }, () => {
if (cb) { cb() }
if (cb) {
cb()
}
})
}
}
};
handleChangeProperty = (property: any) => {
const properties = [...this.state.properties]
const index = properties.findIndex(item => item.id === property.id)
@ -199,7 +220,7 @@ class InterfaceEditor extends Component<
properties.splice(index, 1, property)
this.setState({ properties })
}
}
};
handleChangeInterface = (newItf: any) => {
this.setState({
itf: {
@ -207,41 +228,49 @@ class InterfaceEditor extends Component<
...newItf,
},
})
}
};
handleSaveInterfaceAndProperties = (e: any) => {
e.preventDefault()
const { itf } = this.state
const { updateProperties, updateInterface } = this.props
updateInterface({
id: itf.id,
name: itf.name,
url: itf.url,
method: itf.method,
status: itf.status,
description: itf.description,
}, () => {
/** empty */
})
updateProperties(this.state.itf.id, this.state.properties, this.state.summaryState, () => {
this.handleUnlockInterface()
})
}
updateInterface(
{
id: itf.id,
name: itf.name,
url: itf.url,
method: itf.method,
status: itf.status,
description: itf.description,
},
() => {
/** empty */
}
)
updateProperties(
this.state.itf.id,
this.state.properties,
this.state.summaryState,
() => {
this.handleUnlockInterface()
}
)
};
handleMoveInterface = () => {
this.setState({
moveInterfaceDialogOpen: true,
})
}
};
handleMoveInterfaceSubmit = () => {
/** empty */
}
};
handleLockInterface = () => {
const { itf, lockInterface } = this.props
lockInterface(itf.id)
}
};
handleUnlockInterface = () => {
const { itf, unlockInterface } = this.props
unlockInterface(itf.id)
}
};
}
const mapStateToProps = (state: RootState) => ({

@ -1,173 +1,214 @@
import React, { Component } from 'react'
import { PropTypes, connect, Mock } from '../../family'
import { SmartTextarea } from '../utils'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { YUP_MSG } from '../../family/UIConst'
import { Formik, Field, Form } from 'formik'
import { TextField } from 'formik-material-ui'
import * as Yup from 'yup'
import { Button, Theme, Dialog, Slide, DialogContent, DialogTitle, Select, MenuItem, InputLabel, FormControl } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { Interface, Repository, RootState, Module } from '../../actions/types'
import { updateInterface, addInterface } from '../../actions/interface'
import { refresh } from '../../actions/common'
export const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'HEAD']
export const STATUS_LIST = [200, 301, 403, 404, 500, 502, 503, 504]
// 模拟数据
const mockInterface =
process.env.NODE_ENV === 'development'
? () =>
Mock.mock({
name: '接口@CTITLE(4)',
url: '@URL',
'method|1': METHODS,
description: '@CPARAGRAPH',
repositoryId: undefined,
moduleId: undefined,
})
: () => ({
name: '',
url: '',
method: 'GET',
description: '',
repositoryId: undefined,
moduleId: undefined,
})
type InterfaceFormProps = any
type InterfaceFormState = any
class InterfaceForm extends Component<InterfaceFormProps, InterfaceFormState> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddInterface: PropTypes.func.isRequired,
onUpdateInterface: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
const itf = this.props.itf
this.state = itf ? { ...itf } : mockInterface()
}
render() {
const { rmodal } = this.context
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={1}
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoFocus={true}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={2}
value={this.state.url}
onChange={e => this.setState({ url: e.target.value })}
className="form-control"
placeholder="URI"
spellCheck={false}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<select
name="method"
tabIndex={3}
value={this.state.method}
onChange={e => this.setState({ method: e.target.value })}
className="form-control"
>
{METHODS.map(method => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<select
name="status"
tabIndex={4}
value={this.state.status}
onChange={e => this.setState({ status: e.target.value })}
className="form-control"
>
{STATUS_LIST.map(status => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<SmartTextarea
name="description"
tabIndex={5}
value={this.state.description}
onChange={(e: any) => this.setState({ description: e.target.value })}
className="form-control"
placeholder="Description"
spellCheck={false}
rows="5"
/>
</div>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" variant="contained" color="primary" style={{marginRight: 8}}>
</Button>
<Button onClick={() => rmodal.close()} > </Button>
</div>
</div>
</div>
</form>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
handleSubmit = (e: any) => {
e.preventDefault()
const { onAddInterface, onUpdateInterface } = this.context
const { auth, repository, mod } = this.props
const onAddOrUpdateInterface = this.state.id ? onUpdateInterface : onAddInterface
const itf = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
lockerId: this.state.locker ? this.state.locker.id : null,
})
onAddOrUpdateInterface(itf, () => {
const { rmodal } = this.context
if (rmodal) { rmodal.resolve() }
})
};
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
minHeight: 300,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
}))
const schema = Yup.object().shape<Partial<Interface>>({
name: Yup.string().required(YUP_MSG.REQUIRED).max(20, YUP_MSG.MAX_LENGTH(20)),
description: Yup.string().max(1000, YUP_MSG.MAX_LENGTH(1000)),
})
const FORM_STATE_INIT: Interface = {
id: 0,
name: '',
url: '',
method: 'GET',
description: '',
repositoryId: 0,
moduleId: 0,
status: 200,
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />
})
const mapDispatchToProps = {}
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceForm)
interface Props {
title?: string
open: boolean
onClose: (isOk?: boolean) => void
itf?: Interface
repository?: Repository
mod?: Module
}
function InterfaceForm(props: Props) {
const auth = useSelector((state: RootState) => state.auth)
const { open, onClose, itf, title, repository, mod } = props
const classes = useStyles()
const dispatch = useDispatch()
return (
<Dialog
open={open}
onClose={(_event, reason) => (reason !== 'backdropClick' && onClose())}
TransitionComponent={Transition}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
...(itf || {}),
}}
validationSchema={schema}
onSubmit={values => {
const addOrUpdateInterface = values.id
? updateInterface
: addInterface
const itf: Interface = {
...values,
creatorId: auth.id,
repositoryId: repository!.id,
moduleId: mod!.id,
}
console.log('itf', itf)
dispatch(
addOrUpdateInterface(itf, () => {
dispatch(refresh())
onClose(true)
})
)
}}
render={({ isSubmitting, setFieldValue, values }) => {
return (
<Form>
<div className="rmodal-body">
<div className={classes.formItem}>
<Field
name="name"
label="名称"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<Field
name="url"
label="地址"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<FormControl>
<InputLabel
shrink={true}
htmlFor="method-label-placeholder"
>
</InputLabel>
<Select
value={values.method}
displayEmpty={true}
name="method"
onChange={selected => {
setFieldValue('method', selected.target.value)
}}
>
{METHODS.map(method => (
<MenuItem key={method} value={method}>
{method}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className={classes.formItem}>
<InputLabel
shrink={true}
htmlFor="method-label-placeholder"
>
</InputLabel>
<Select
value={values.status}
displayEmpty={true}
name="status"
onChange={selected => {
setFieldValue('status', selected.target.value)
}}
>
{STATUS_LIST.map(status => (
<MenuItem key={status} value={status}>
{status}
</MenuItem>
))}
</Select>
</div>
<div className={classes.formItem}>
<Field
name="description"
label="简介"
component={TextField}
multiline={true}
rows={4}
fullWidth={true}
/>
</div>
</div>
<div className={classes.ctl}>
<Button
type="submit"
variant="contained"
color="primary"
className="mr1"
disabled={isSubmitting}
>
</Button>
<Button onClick={() => onClose()} disabled={isSubmitting}>
</Button>
</div>
</Form>
)
}}
/>
</div>
</DialogContent>
</Dialog>
)
}
export default InterfaceForm

@ -1,199 +1,196 @@
import React, { Component } from 'react'
import React, { useState, MouseEventHandler } from 'react'
import {
connect,
Link,
replace,
StoreStateRouterLocationURI
StoreStateRouterLocationURI,
replace
} from '../../family'
import { sortInterfaceList } from '../../actions/interface'
import { RModal, RSortable } from '../utils'
import { sortInterfaceList, deleteInterface } from '../../actions/interface'
import { RSortable } from '../utils'
import InterfaceForm from './InterfaceForm'
import { GoPencil, GoTrashcan, GoLock } from 'react-icons/go'
import { getCurrentInterface } from '../../selectors/interface'
import PropTypes from 'prop-types'
import Button from '@material-ui/core/Button'
import { useSelector, useDispatch } from 'react-redux'
import './InterfaceList.css'
import { RootState } from 'actions/types'
type InterfaceProps = any
type InterfaceState = any
class InterfaceBase extends Component<InterfaceProps, InterfaceState> {
static contextTypes = {
store: PropTypes.object,
onDeleteInterface: PropTypes.func.isRequired,
}
constructor(props: any) {
super(props)
this.state = { update: false }
}
render() {
const { auth, repository, mod, itf, router } = this.props
const selectHref = StoreStateRouterLocationURI(router)
.setSearch('itf', itf.id)
.href()
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((i: any) => i.id === auth.id)
return (
<div className="Interface clearfix">
<span>
<Link
to={selectHref}
onClick={e => {
if (
this.props.curItf &&
this.props.curItf.locker &&
!window.confirm(
'编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?'
)
) {
e.preventDefault()
}
}}
>
<div className="name">{itf.name}</div>
<div className="url">{itf.url}</div>
</Link>
</span>
{isOwned || isJoined ? (
<div className="toolbar">
{itf.locker ? (
<span className="locked mr5">
<GoLock />
</span>
) : null}
{!itf.locker || itf.locker.id === auth.id ? (
<span
className="fake-link"
onClick={() => this.setState({ update: true })}
>
<GoPencil />
</span>
) : null}
<RModal
when={this.state.update}
onClose={() => this.setState({ update: false })}
onResolve={this.handleUpdate}
>
<InterfaceForm
title="修改接口"
repository={repository}
mod={mod}
itf={itf}
/>
</RModal>
{!itf.locker ? (
<Link to="" onClick={e => this.handleDelete(e, itf)}>
<GoTrashcan />
</Link>
) : null}
</div>
) : null}
</div>
)
}
handleDelete = (e: any, itf: any) => {
import {
Module,
Repository,
RootState,
Interface,
User
} from '../../actions/types'
import { refresh } from '../../actions/common'
interface InterfaceBaseProps {
repository: Repository
mod: Module
active?: boolean
auth?: User
itf?: Interface
curItf?: Interface
deleteInterface: typeof deleteInterface
replace?: typeof replace
}
function InterfaceBase(props: InterfaceBaseProps) {
const { repository, mod, itf, curItf } = props
const auth = useSelector((state: RootState) => state.auth)
const router = useSelector((state: RootState) => state.router)
const selectHref = StoreStateRouterLocationURI(router)
.setSearch('itf', itf!.id.toString())
.href()
const isOwned = repository.owner!.id === auth.id
const isJoined = repository.members!.find((i: any) => i.id === auth.id)
const [open, setOpen] = useState(false)
const dispatch = useDispatch()
const handleDeleteInterface: MouseEventHandler<HTMLAnchorElement> = e => {
e.preventDefault()
const message = `接口被删除后不可恢复!\n确认继续删除『#${itf.id} ${
itf.name
const message = `接口被删除后不可恢复!\n确认继续删除『#${itf!.id} ${
itf!.name
}`
if (window.confirm(message)) {
const { onDeleteInterface } = this.context
onDeleteInterface(itf.id, () => {
const { store } = this.context
const uri = StoreStateRouterLocationURI(store)
const deleteHref = this.props.active
? uri.removeSearch('itf').href()
: uri.href()
store.dispatch(replace(deleteHref))
const { deleteInterface } = props
deleteInterface(props.itf!.id, () => {
dispatch(refresh())
})
const { pathname, hash, search } = router.location
replace(pathname + hash + search)
}
};
handleUpdate = () => {
/** test */
};
}
return (
<div className="Interface clearfix">
<span>
<Link
to={selectHref}
onClick={e => {
if (
curItf &&
curItf.locker &&
!window.confirm(
'编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?'
)
) {
e.preventDefault()
}
}}
>
<div className="name">{itf!.name}</div>
<div className="url">{itf!.url}</div>
</Link>
</span>
{isOwned || isJoined ? (
<div className="toolbar">
{itf!.locker ? (
<span className="locked mr5">
<GoLock />
</span>
) : null}
{!itf!.locker || itf!.locker.id === auth.id ? (
<span className="fake-link" onClick={() => setOpen(true)}>
<GoPencil />
</span>
) : null}
<InterfaceForm
title="修改接口"
repository={repository}
mod={mod}
itf={itf}
open={open}
onClose={() => setOpen(false)}
/>
{!itf!.locker ? (
<Link to="" onClick={handleDeleteInterface}>
<GoTrashcan />
</Link>
) : null}
</div>
) : null}
</div>
)
}
const mapStateToProps = (state: RootState) => ({
curItf: getCurrentInterface(state),
router: state.router,
})
const mapDispatchToProps = {
replace,
onSortInterfaceList: sortInterfaceList,
deleteInterface,
}
const InterfaceWrap = connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceBase)
const Interface = connect((state: any) => ({ router: state.router }))(
InterfaceBase
)
type InterfaceListProps = any
type InterfaceListState = any
class InterfaceList extends Component<InterfaceListProps, InterfaceListState> {
constructor(props: any) {
super(props)
this.state = { create: false }
interface InterfaceListProps {
itfs?: Interface[]
itf?: Interface
curItf: Interface
mod: Module
repository: Repository
}
function InterfaceList(props: InterfaceListProps, context: any) {
const [open, setOpen] = useState(false)
const auth = useSelector((state: RootState) => state.auth)
const { repository, itf, itfs = [], mod } = props
const isOwned = repository.owner!.id === auth.id
const isJoined = repository.members!.find((item: any) => item.id === auth.id)
const handleSort = (_: any, sortable: any) => {
const { onSortInterfaceList } = context
onSortInterfaceList(sortable.toArray())
}
render() {
const { auth, repository, mod, itfs = [], itf, curItf } = this.props
if (!mod.id) {
return null
}
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((i: any) => i.id === auth.id)
return (
<article className="InterfaceList">
{isOwned || isJoined ? (
<div className="header">
<Button
variant="outlined"
fullWidth={true}
color="primary"
onClick={() => this.setState({ create: true })}
>
</Button>
<RModal
when={this.state.create}
onClose={() => this.setState({ create: false })}
onResolve={this.handleCreate}
>
<InterfaceForm
title="新建接口"
repository={repository}
mod={mod}
/>
</RModal>
</div>
) : null}
<RSortable onChange={this.handleSort} disabled={!isOwned && !isJoined}>
return (
<article className="InterfaceList">
{isOwned || isJoined ? (
<div className="header">
<Button
variant="outlined"
fullWidth={true}
color="primary"
onClick={() => setOpen(true)}
>
</Button>
<InterfaceForm
title="新建接口"
repository={repository}
mod={mod}
open={open}
onClose={() => setOpen(false)}
/>
</div>
) : null}
{itfs.length ? (
<RSortable onChange={handleSort} disabled={!isOwned && !isJoined}>
<ul className="body">
{itfs.map((item: any) => (
<li
key={item.id}
className={item.id === itf.id ? 'active sortable' : 'sortable'}
className={item.id === itf!.id ? 'active sortable' : 'sortable'}
data-id={item.id}
>
<Interface
<InterfaceWrap
repository={repository}
mod={mod}
itf={item}
active={item.id === itf.id}
active={item.id === itf!.id}
auth={auth}
curItf={curItf}
// curItf={curItf}
/>
</li>
))}
</ul>
</RSortable>
</article>
)
}
handleCreate = () => {
/** empty */
};
handleSort = (_: any, sortable: any) => {
const { onSortInterfaceList, mod } = this.props
onSortInterfaceList(sortable.toArray(), mod.id)
};
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
curItf: getCurrentInterface(state),
router: state.router,
})
const mapDispatchToProps = {
onSortInterfaceList: sortInterfaceList,
) : (
<div className="alert alert-info"></div>
)}
</article>
)
}
export default connect(
mapStateToProps,
mapDispatchToProps

@ -23,7 +23,6 @@ class Previewer extends Component<any, any> {
const { label, scope, properties, itf } = this.props
try {
// DONE 2.2 支持引用请求参数
scopedProperties = {
request: properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === 'request'),

@ -1,3 +1,42 @@
.label.fl
float: left;
.ovh
overflow: hidden;
.SyncRoomDialog
min-width: 400px;
min-height: 300px;
padding: 1.64rem;
ul
margin-top: 2rem;
padding: 0;
li
display: block;
border-radius: 5px;
padding: 1rem;
border: 1px solid #CCC;
li + li
margin-top: 1rem;
i
font-style: normal;
color: #FFF;
padding: 0 10px;
background: #CCC;
margin-left: 1rem;
border-radius: 5px;
zoom: .7;
p
margin-bottom: 0;
.button
float: right;
margin-top: 7px;
font-size: 14px;
background: #28a745;
color: #FFF;
line-height: 1.5;
user-select: none;
padding: .375rem 2rem;
border-radius: .25rem;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
.InterfaceSummary
.dropdown
display: inline-block
@ -5,6 +44,11 @@
max-width: 500px
.body-options
margin: 8px
display: flex
.form-check
margin-right: 10px
.form-check-label, .form-check-input
cursor: pointer
ul.summary
padding: 0
li
@ -12,5 +56,49 @@
margin: 0
padding: 0
margin-bottom: 5px
.nav-tabs
margin-top: 20px
.nav
display: -ms-flexbox
display: flex
-ms-flex-wrap: wrap
flex-wrap: wrap
padding-left: 0
margin-bottom: 0
list-style: none
.nav-link
display: block
padding: 0.5rem 1rem
cursor: pointer
&:hover, &:focus
text-decoration: none
&.disabled
color: #6c757d
pointer-events: none
cursor: default
.nav-tabs
border-bottom: 1px solid #dee2e6
.nav-item
margin-bottom: -1px
.nav-link
border: 1px solid transparent
border-top-left-radius: 0.25rem
border-top-right-radius: 0.25rem
&:hover, &:focus
border-color: #e9ecef #e9ecef #dee2e6
&.disabled
color: #6c757d
background-color: transparent
border-color: transparent
&.active
color: #495057
background-color: #fff
border-color: #dee2e6 #dee2e6 #fff
.nav-item.show .nav-link
color: #495057
background-color: #fff
border-color: #dee2e6 #dee2e6 #fff
.dropdown-menu
margin-top: -1px
border-top-left-radius: 0
border-top-right-radius: 0

@ -1,13 +1,27 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { replace, StoreStateRouterLocationURI, PropTypes } from '../../family'
import copy from 'clipboard-copy'
import { GlobalHotKeys } from 'react-hotkeys'
import {
replace,
StoreStateRouterLocationURI,
PropTypes
} from '../../family'
import { serve } from '../../relatives/services/constant'
import { METHODS, STATUS_LIST } from './InterfaceForm'
import { CopyToClipboard } from '../utils/'
import { getRelativeUrl } from '../../utils/URLUtils'
import './InterfaceSummary.css'
import { RootState } from 'actions/types'
import { TextField, Select, FormControl, InputLabel, Input, MenuItem } from '@material-ui/core'
import { showMessage, MSG_TYPE } from 'actions/common'
import {
TextField,
Select,
FormControl,
InputLabel,
Input,
MenuItem
} from '@material-ui/core'
export const BODY_OPTION = {
FORM_DATA: 'FORM_DATA',
@ -29,19 +43,69 @@ export function rptFromStr2Num(rpt: any) {
}
return pos
}
function url2name(itf: any) {
// copy from http://gitlab.alibaba-inc.com/thx/magix-cli/blob/master/platform/rap.js#L306
const method = itf.method.toLowerCase()
const apiUrl = itf.url
const projectId = itf.repositoryId
const id = itf.id
const regExp = /^(?:https?:\/\/[^\/]+)?(\/?.+?\/?)(?:\.[^./]+)?$/
const regExpExec = regExp.exec(apiUrl)
if (!regExpExec) {
return {
ok: false,
name: '',
message: `\n ✘ 您的rap接口url设置格式不正确参考格式/api/test.json (接口url:${apiUrl}, 项目id:${projectId}, 接口id:${id})\n`,
}
}
const urlSplit = regExpExec[1].split('/')
// 接口地址为RESTful的清除占位符
// api/:id/get -> api//get
// api/bid[0-9]{4}/get -> api//get
urlSplit.forEach((item, i) => {
if (/\:id/.test(item)) {
urlSplit[i] = '$id'
} else if (/[\[\]\{\}]/.test(item)) {
urlSplit[i] = '$regx'
}
})
// 只去除第一个为空的值,最后一个为空保留
// 有可能情况是接口 /api/login 以及 /api/login/ 需要同时存在
if (urlSplit[0].trim() === '') {
urlSplit.shift()
}
urlSplit.push(method)
const urlToName = urlSplit.join('_')
return {
ok: true,
name: urlToName,
message: '',
}
}
type InterfaceSummaryProps = {
store: object,
handleChangeInterface: (itf: any) => void,
[x: string]: any,
store: object;
handleChangeInterface: (itf: any) => void;
showMessage: typeof showMessage;
[x: string]: any;
}
type InterfaceSummaryState = {
bodyOption?: any,
requestParamsType?: any,
method?: any,
status?: any,
[x: string]: any,
bodyOption?: any;
requestParamsType?: any;
method?: any;
status?: any;
[x: string]: any;
}
class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummaryState> {
class InterfaceSummary extends Component<
InterfaceSummaryProps,
InterfaceSummaryState
> {
static contextTypes = {
// onAddForeignRoomCase: PropTypes.func.isRequired,
onDeleteInterface: PropTypes.func.isRequired,
@ -50,13 +114,18 @@ class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummary
super(props)
this.state = {
bodyOption: BODY_OPTION.FORM_DATA,
requestParamsType: props.itf.method === 'POST' ? REQUEST_PARAMS_TYPE.BODY_PARAMS : REQUEST_PARAMS_TYPE.QUERY_PARAMS,
requestParamsType:
props.itf.method === 'POST'
? REQUEST_PARAMS_TYPE.BODY_PARAMS
: REQUEST_PARAMS_TYPE.QUERY_PARAMS,
}
this.changeMethod = this.changeMethod.bind(this)
this.changeHandler = this.changeHandler.bind(this)
this.switchBodyOption = this.switchBodyOption.bind(this)
this.switchRequestParamsType = this.switchRequestParamsType.bind(this)
this.state.requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS && props.stateChangeHandler(this.state)
this.copyModelName = this.copyModelName.bind(this)
this.state.requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS &&
props.stateChangeHandler(this.state)
}
switchBodyOption(val: any) {
return () => {
@ -93,91 +162,159 @@ class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummary
[e.target.name]: e.target.value,
})
}
copyModelName() {
const { itf = {} } = this.props
const res = url2name(itf)
if (!res.ok) {
this.props.showMessage(`复制失败: ${res.message}`, MSG_TYPE.ERROR)
return
}
const modelName = res.name
copy(modelName)
.then(() => {
this.props.showMessage(
`成功复制 ${modelName} 到剪贴板`,
MSG_TYPE.SUCCESS
)
})
.catch(() => {
this.props.showMessage(`复制失败`, MSG_TYPE.ERROR)
})
}
render() {
const { repository = {}, itf = {}, editable, handleChangeInterface } = this.props
const {
repository = {},
itf = {},
editable,
handleChangeInterface,
} = this.props
const { requestParamsType } = this.state
if (!itf.id) { return null }
const keyMap = {
COPY_MODEL_NAME: ['ctrl+alt+c'],
}
const handlers = {
COPY_MODEL_NAME: this.copyModelName,
}
if (!itf.id) {
return null
}
return (
<div className="InterfaceSummary">
{!editable && <div className="header">
<CopyToClipboard text={itf.name}>
<span className="title">{itf.name}</span>
</CopyToClipboard>
</div>}
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
{!editable && (
<div className="header">
<CopyToClipboard text={itf.name}>
<span className="title">{itf.name}</span>
</CopyToClipboard>
</div>
)}
<ul className="summary">
{editable ? <>
<li style={{width: '50%'}}>
<TextField
style={{marginTop: 0}}
id="name"
label="名称"
value={itf.name}
fullWidth={true}
autoComplete="off"
onChange={(e) => {handleChangeInterface({name: e.target.value})}}
margin="normal"
/>
</li>
<li style={{width: '50%'}}>
<TextField
id="url"
label="地址"
value={itf.url}
fullWidth={true}
autoComplete="off"
onChange={(e) => {handleChangeInterface({url: e.target.value})}}
margin="normal"
/>
</li>
<li style={{marginTop: 24}}>
<FormControl>
<InputLabel shrink={true} htmlFor="method-label-placeholder">
</InputLabel>
<Select
value={itf.method}
input={<Input name="method" id="method-label-placeholder" />}
onChange={(e) => {handleChangeInterface({method: e.target.value})}}
displayEmpty={true}
name="method"
>
{METHODS.map(method => <MenuItem key={method} value={method}>{method}</MenuItem>)}
</Select>
</FormControl>
<FormControl style={{marginLeft: 20}}>
<InputLabel shrink={true} htmlFor="status-label-placeholder">
</InputLabel>
<Select
value={itf.status}
input={<Input name="status" id="status-label-placeholder" />}
onChange={(e) => {handleChangeInterface({status: e.target.value})}}
displayEmpty={true}
name="status"
>
{STATUS_LIST.map(status => <MenuItem key={status} value={status}>{status}</MenuItem>)}
</Select>
</FormControl>
</li>
<li style={{width: '50%'}}>
<TextField
id="description"
label="描述(可多行)"
value={itf.description}
fullWidth={true}
multiline={true}
autoComplete="off"
onChange={(e) => {handleChangeInterface({description: e.target.value})}}
margin="normal"
/>
</li>
</> : <>
{editable ? (
<>
<li style={{ width: '50%' }}>
<TextField
style={{ marginTop: 0 }}
id="name"
label="名称"
value={itf.name}
fullWidth={true}
autoComplete="off"
onChange={e => {
handleChangeInterface({ name: e.target.value })
}}
margin="normal"
/>
</li>
<li style={{ width: '50%' }}>
<TextField
id="url"
label="地址"
value={itf.url}
fullWidth={true}
autoComplete="off"
onChange={e => {
handleChangeInterface({ url: e.target.value })
}}
margin="normal"
/>
</li>
<li style={{ marginTop: 24 }}>
<FormControl>
<InputLabel shrink={true} htmlFor="method-label-placeholder">
</InputLabel>
<Select
value={itf.method}
input={
<Input name="method" id="method-label-placeholder" />
}
onChange={e => {
handleChangeInterface({ method: e.target.value })
}}
displayEmpty={true}
name="method"
>
{METHODS.map(method => (
<MenuItem key={method} value={method}>
{method}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl style={{ marginLeft: 20 }}>
<InputLabel shrink={true} htmlFor="status-label-placeholder">
</InputLabel>
<Select
value={itf.status}
input={
<Input name="status" id="status-label-placeholder" />
}
onChange={e => {
handleChangeInterface({ status: e.target.value })
}}
displayEmpty={true}
name="status"
>
{STATUS_LIST.map(status => (
<MenuItem key={status} value={status}>
{status}
</MenuItem>
))}
</Select>
</FormControl>
</li>
<li style={{ width: '50%' }}>
<TextField
id="description"
label="描述(可多行)"
value={itf.description}
fullWidth={true}
multiline={true}
autoComplete="off"
onChange={e => {
handleChangeInterface({ description: e.target.value })
}}
margin="normal"
/>
</li>
</>
) : (
<>
<li>
<CopyToClipboard text={itf.url}>
<span>
<span className="label"></span>
<a href={`${serve}/app/mock/${repository.id}${getRelativeUrl(itf.url || '')}`} target="_blank" rel="noopener noreferrer">{itf.url}</a>
<a
href={`${serve}/app/mock/${repository.id}${getRelativeUrl(
itf.url || ''
)}`}
target="_blank"
rel="noopener noreferrer"
>
{itf.url}
</a>
</span>
</CopyToClipboard>
</li>
@ -207,50 +344,125 @@ class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummary
</CopyToClipboard>
</li>
)}
</>
}
</>
)}
</ul>
{editable && (
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.HEADERS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.HEADERS ? 'active' : ''}`} role="tab" data-toggle="tab">
headers
</button>
</li>
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.QUERY_PARAMS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS ? 'active' : ''}`} role="tab" data-toggle="tab">
Query Params
</button>
</li>
<li className="nav-item" onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.BODY_PARAMS)}>
<button className={`nav-link ${requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? 'active' : ''}`} role="tab" data-toggle="tab">
Body Params
</button>
</li>
</ul>
)}
<ul className="nav nav-tabs" role="tablist">
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.HEADERS
)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.HEADERS
? 'active'
: ''
}`}
role="tab"
data-toggle="tab"
>
headers
</button>
</li>
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.QUERY_PARAMS
)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS
? 'active'
: ''
}`}
role="tab"
data-toggle="tab"
>
Query Params
</button>
</li>
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.BODY_PARAMS
)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS
? 'active'
: ''
}`}
role="tab"
data-toggle="tab"
>
Body Params
</button>
</li>
</ul>
)}
{editable && requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? (
<div className="body-options">
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio1" value="option1" />
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}
>
<input
className="form-check-input"
type="radio"
name="inlineRadioOptions"
id="inlineRadio1"
value="option1"
/>
<label className="form-check-label" htmlFor="inlineRadio1">
form-data
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.FORM_URLENCODED)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio2" value="option2" />
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.FORM_URLENCODED)}
>
<input
className="form-check-input"
type="radio"
name="inlineRadioOptions"
id="inlineRadio2"
value="option2"
/>
<label className="form-check-label" htmlFor="inlineRadio2">
x-www-form-urlencoded
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.RAW)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio3" value="option3" />
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.RAW)}
>
<input
className="form-check-input"
type="radio"
name="inlineRadioOptions"
id="inlineRadio3"
value="option3"
/>
<label className="form-check-label" htmlFor="inlineRadio3">
raw
</label>
</div>
<div className="form-check form-check-inline" onClick={this.switchBodyOption(BODY_OPTION.BINARY)}>
<input className="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio4" value="option4" />
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.BINARY)}
>
<input
className="form-check-input"
type="radio"
name="inlineRadioOptions"
id="inlineRadio4"
value="option4"
/>
<label className="form-check-label" htmlFor="inlineRadio4">
binary
</label>
@ -268,7 +480,9 @@ class InterfaceSummary extends Component<InterfaceSummaryProps, InterfaceSummary
onDeleteInterface(itf.id, () => {
const { router, replace } = this.props
const uri = StoreStateRouterLocationURI(router)
const deleteHref = this.props.active ? uri.removeSearch('itf').href() : uri.href()
const deleteHref = this.props.active
? uri.removeSearch('itf').href()
: uri.href()
replace(deleteHref)
})
}
@ -291,5 +505,9 @@ const mapStateToProps = (state: RootState) => ({
})
const mapDispatchToProps = {
replace,
showMessage,
}
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceSummary)
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceSummary)

@ -1,114 +1,150 @@
import React, { Component } from 'react'
import { PropTypes, connect, Mock } from '../../family'
import { SmartTextarea } from '../utils'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
// 模拟数据
const mockModule = process.env.NODE_ENV === 'development'
? () => Mock.mock({
name: '模块@CTITLE(4)',
description: '@CPARAGRAPH',
repositoryId: undefined,
})
: () => ({
name: '',
description: '',
repositoryId: undefined,
})
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { YUP_MSG } from '../../family/UIConst'
import { Formik, Field, Form } from 'formik'
import { TextField } from 'formik-material-ui'
import * as Yup from 'yup'
import { Button, Theme, Dialog, Slide, DialogContent, DialogTitle } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { Module, Repository, RootState } from '../../actions/types'
import { updateModule, addModule } from '../../actions/module'
import { refresh } from '../../actions/common'
// 展示组件
class ModuleForm extends Component<any, any> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
onAddModule: PropTypes.func.isRequired,
onUpdateModule: PropTypes.func.isRequired,
}
static propTypes = {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object,
}
constructor(props: any) {
super(props)
const { mod } = this.props
this.state = mod ? { ...mod } : mockModule()
}
render() {
const { rmodal } = this.context
return (
<section>
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
tabIndex={1}
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoFocus={true}
required={true}
/>
</div>
</div>
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<SmartTextarea
tabIndex={2}
name="description"
value={this.state.description}
onChange={(e: any) => this.setState({ description: e.target.value })}
className="form-control"
placeholder="Description"
spellCheck={false}
rows="5"
/>
</div>
</div>
</div>
<div className="rmodal-footer">
<div className="form-group row mb0">
<label className="col-sm-2 control-label" />
<div className="col-sm-10">
<Button type="submit" style={{ marginRight: 8 }} variant="contained" color="primary"></Button>
<Button onClick={() => rmodal.close()} ></Button>
</div>
</div>
</div>
</form>
</section>
)
}
handleSubmit = (e: any) => {
e.preventDefault()
const { onAddModule, onUpdateModule } = this.context
const { auth, repository } = this.props
const onAddOrUpdateModule = this.state.id ? onUpdateModule : onAddModule
const mod = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
})
const { rmodal } = this.context
rmodal.close()
onAddOrUpdateModule(mod, () => {
if (rmodal) { rmodal.resolve() }
})
}
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
}))
const schema = Yup.object().shape<Partial<Module>>({
name: Yup.string().required(YUP_MSG.REQUIRED).max(20, YUP_MSG.MAX_LENGTH(20)),
description: Yup.string().max(1000, YUP_MSG.MAX_LENGTH(1000)),
})
const FORM_STATE_INIT: Module = {
id: 0,
name: '',
description: '',
repositoryid: 0,
priority: 1,
}
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />
})
const mapDispatchToProps = ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModuleForm)
interface Props {
title?: string
open: boolean
onClose: (isOk?: boolean) => void
module?: Module
repository?: Repository
}
function ModuleForm(props: Props) {
const auth = useSelector((state: RootState) => state.auth)
const { open, onClose, module, title, repository } = props
const classes = useStyles()
const dispatch = useDispatch()
return (
<Dialog
open={open}
onClose={(_event, reason) => (reason !== 'backdropClick' && onClose())}
TransitionComponent={Transition}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
...(module || {}),
}}
validationSchema={schema}
onSubmit={(values) => {
const addOrUpdateModule = values.id ? updateModule : addModule
const module: Module = {
...values,
creatorId: auth.id,
repositoryId: repository!.id,
}
dispatch(addOrUpdateModule(module, () => {
dispatch(refresh())
onClose(true)
}))
}}
render={({ isSubmitting }) => {
return (
<Form>
<div className="rmodal-body">
<div className={classes.formItem}>
<Field
name="name"
label="模块名称"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<Field
name="description"
label="模块简介"
component={TextField}
multiline={true}
rows={5}
fullWidth={true}
/>
</div>
</div>
<div className={classes.ctl}>
<Button
type="submit"
variant="contained"
color="primary"
className="mr1"
disabled={isSubmitting}
>
</Button>
<Button
onClick={() => onClose()}
disabled={isSubmitting}
>
</Button>
</div>
</Form>
)
}}
/>
</div>
</DialogContent>
</Dialog>
)
}
export default ModuleForm

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

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

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

@ -1,10 +1,16 @@
import React, { Component } from 'react'
import { PropTypes, Link } from '../../family'
import { Tree, SmartTextarea, RModal, RSortable, CopyToClipboard } from '../utils'
import {
Tree,
SmartTextarea,
RModal,
RSortable,
CopyToClipboard
} from '../utils'
import PropertyForm from './PropertyForm'
import Importer from './Importer'
import Previewer from './InterfacePreviewer'
import { GoPlus, GoTrashcan, GoQuestion } from 'react-icons/go'
import { GoPlus, GoTrashcan, GoQuestion, GoChevronDown, GoChevronRight } from 'react-icons/go'
import { rptFromStr2Num } from './InterfaceSummary'
import './PropertyList.css'
import { ButtonGroup, Button, Checkbox } from '@material-ui/core'
@ -14,32 +20,34 @@ import Mock from 'mockjs'
import JSON5 from 'json5'
import { elementInViewport } from 'utils/ElementInViewport'
const mockProperty = process.env.NODE_ENV === 'development'
? () => Mock.mock({
'scope|1': ['request', 'response'],
name: '@WORD(6)',
'type|1': ['String', 'Number', 'Boolean'],
'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,
})
const mockProperty =
process.env.NODE_ENV === 'development'
? () =>
Mock.mock({
'scope|1': ['request', 'response'],
name: '@WORD(6)',
'type|1': ['String', 'Number', 'Boolean'],
'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,
})
export const RequestPropertyListPreviewer = (props: any) => (
<Previewer {...props}/>
<Previewer {...props} />
)
export const ResponsePropertyListPreviewer = (props: any) => (
@ -60,19 +68,17 @@ class SortableTreeTableHeader extends Component<any, any> {
<div className="SortableTreeTableHeader">
<div className="flex-row">
{/* DONE 2.1 每列增加帮助 Tip */}
{editable && (
<div className="th operations">
<Link
to=""
onClick={e => {
e.preventDefault()
handleClickCreatePropertyButton()
}}
>
<GoPlus className="fontsize-14 color-6" />
</Link>
</div>
)}
<div className="th operations">
<Link
to=""
onClick={e => {
e.preventDefault()
handleClickCreatePropertyButton()
}}
>
{editable && <GoPlus className="fontsize-14 color-6" />}
</Link>
</div>
<div className="th name"></div>
<div className="th type"></div>
<div className="th type"></div>
@ -108,8 +114,13 @@ const PropertyLabel = (props: any) => {
}
}
// const PropertyArrow = (props: { property: any }) => {}
const getFormattedValue = (itf: any) => {
if ((itf.type === 'Array' || itf.type === 'Object' || itf.type === 'String') && itf.value) {
if (
(itf.type === 'Array' || itf.type === 'Object' || itf.type === 'String') &&
itf.value
) {
try {
const formatted = JSON.stringify(JSON5.parse(itf.value), undefined, 2)
return formatted
@ -120,17 +131,53 @@ const getFormattedValue = (itf: any) => {
return itf.value || ''
}
}
class SortableTreeTableRow extends Component<any, any> {
interface SortableTreeTableRowState {
property: {
children: any[];
[k: string]: any;
}
interfaceId: number
childrenAdded: boolean
childrenExpandingIdList: number[]
}
interface SortableTreeTableRowProps {
/** 当前层级是不是展开 */
isExpanding: boolean
interfaceId: number
[k: string]: any
}
class SortableTreeTableRow extends Component<
SortableTreeTableRowProps,
SortableTreeTableRowState
> {
focusNameInput: HTMLInputElement | undefined = undefined
state = {
property: { children: [] },
childrenAdded: false,
constructor(props: SortableTreeTableRowProps) {
super(props)
this.state = {
property: { children: [] },
childrenAdded: false,
childrenExpandingIdList: [],
interfaceId: -1,
}
}
static getDerivedStateFromProps(nextProps: any, prevState: any) {
return {
interfaceId: nextProps.interfaceId,
property: nextProps.property,
childrenAdded: nextProps.property.children.length > prevState.property.children.length,
childrenAdded:
nextProps.property.children.length > prevState.property.children.length,
childrenExpandingIdList:
nextProps.interfaceId !== prevState.interfaceId
? nextProps.property.children
// 默认展现 012 三个层级
.filter(
(item: any) =>
item.children &&
item.children.length > 0 &&
nextProps.property.depth < 1
)
.map((item: any) => item.id)
: prevState.childrenExpandingIdList,
}
}
componentDidMount() {
@ -143,141 +190,289 @@ class SortableTreeTableRow extends Component<any, any> {
if (this.focusNameInput && this.state.childrenAdded) {
this.focusNameInput.focus()
if (!elementInViewport(this.focusNameInput)) {
this.focusNameInput.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
this.focusNameInput.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
})
}
}
}
render() {
const { property, editable, handleClickCreateChildPropertyButton, highlightId,
handleDeleteMemoryProperty, handleChangePropertyField, handleSortProperties } = this.props
const {
property,
isExpanding,
editable,
handleClickCreateChildPropertyButton,
highlightId,
handleDeleteMemoryProperty,
handleChangePropertyField,
handleSortProperties,
} = this.props
return (
<RSortable group={property.depth} handle=".SortableTreeTableRow" disabled={!editable} onChange={handleSortProperties}>
<div className={`RSortableWrapper depth${property.depth}`}>
{property.children.sort((a: any, b: any) => a.priority - b.priority).map((item: any) =>
<div key={item.id} className="SortableTreeTableRow" data-id={item.id}>
<div className={classNames('flex-row', { highlight: item.id === highlightId })}>
{editable &&
<div className="td operations nowrap">
{(item.type === 'Object' || item.type === 'Array')
? <Link
to=""
onClick={e => { e.preventDefault(); handleClickCreateChildPropertyButton(item) }}
isExpanding && (
<RSortable
group={property.depth}
handle=".SortableTreeTableRow"
disabled={!editable}
onChange={handleSortProperties}
>
<div className={`RSortableWrapper depth${property.depth}`}>
{property.children
.sort((a: any, b: any) => a.priority - b.priority)
.map((item: any) => {
const childrenIsExpanding = this.state.childrenExpandingIdList.includes(
item.id
)
return (
<div
key={item.id}
className="SortableTreeTableRow"
data-id={item.id}
>
<div
className={classNames('flex-row', {
highlight: item.id === highlightId,
})}
>
<div className="td operations nowrap">
{(item.type === 'Object' || item.type === 'Array') &&
item.children &&
item.children.length ? (
<Link
to=""
onClick={e => {
e.preventDefault()
this.setState(prev => ({
...prev,
childrenExpandingIdList: childrenIsExpanding
? prev.childrenExpandingIdList.filter(
id => id !== item.id
)
: [...prev.childrenExpandingIdList, item.id],
}))
}}
>
{childrenIsExpanding ? (
<GoChevronDown className="fontsize-14 color-6"/>
) : (
<GoChevronRight className="fontsize-14 color-6"/>
)}
</Link>
) : null}
{editable && (
<>
{item.type === 'Object' || item.type === 'Array' ? (
<Link
to=""
onClick={e => {
e.preventDefault()
handleClickCreateChildPropertyButton(item)
this.setState(prev => ({
...prev,
childrenExpandingIdList: _.uniq([
...prev.childrenExpandingIdList,
item.id,
]),
}))
}}
>
<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`}
>
<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
?
<>
<CopyToClipboard text={item.name}><span className="nowrap">{item.name}</span></CopyToClipboard>
{item.scope === 'request' && item.depth === 0 ?
<div style={{ float: 'right' }}><PropertyLabel pos={item.pos} /></div> : null}
</>
: <input
ref={(input: HTMLInputElement) => {
if (item.id === highlightId) {
this.focusNameInput = 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 required type depth-${item.depth} nowrap`}>
<Checkbox
checked={!!item.required}
disabled={!editable}
onChange={e =>
handleChangePropertyField(
item.id,
'required',
e.target.checked
)
}
color="primary"
inputProps={{
'aria-label': '必选',
}}
/>
</div>
{!editable ? (
<>
<CopyToClipboard text={item.name} type="right">
<span className="name-wrapper nowrap">
{item.name}
</span>
</CopyToClipboard>
{item.scope === 'request' && item.depth === 0 ? (
<div style={{ margin: '1px 0 0 3px' }}>
<PropertyLabel pos={item.pos} />
</div>
) : null}
</>
) : (
<input
ref={(input: HTMLInputElement) => {
if (item.id === highlightId) {
this.focusNameInput = 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 required type depth-${item.depth} nowrap`}
>
<Checkbox
checked={!!item.required}
disabled={!editable}
onChange={e =>
handleChangePropertyField(
item.id,
'required',
e.target.checked
)
}
color="primary"
inputProps={{
'aria-label': '必选',
}}
/>
</div>
<div className="td payload type">
{!editable
? <CopyToClipboard text={item.type.toLowerCase()}><span className="nowrap">{item.type}</span></CopyToClipboard>
: <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
? <CopyToClipboard text={item.value}><span className="value-container">{getFormattedValue(item)}</span></CopyToClipboard>
: <SmartTextarea
value={item.value || ''}
onChange={(e: any) => handleChangePropertyField(item.id, 'value', e.target.value)}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
<div className="td payload desc">
{!editable
? <CopyToClipboard text={item.description}><span>{item.description}</span></CopyToClipboard>
: <SmartTextarea
value={item.description || ''}
onChange={(e: any) => handleChangePropertyField(item.id, 'description', e.target.value)}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
}
</div>
</div>
{item.children && item.children.length ? <SortableTreeTableRow {...this.props} property={item} /> : null}
</div>
)}
</div>
</RSortable>
<div className="td payload type">
{!editable ? (
<CopyToClipboard text={item.type}>
<span className="nowrap">{item.type}</span>
</CopyToClipboard>
) : (
<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 ? (
<CopyToClipboard text={item.value}>
<span className="value-container">
{getFormattedValue(item)}
</span>
</CopyToClipboard>
) : (
<SmartTextarea
value={item.value || ''}
onChange={(e: any) =>
handleChangePropertyField(
item.id,
'value',
e.target.value
)
}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
)}
</div>
<div className="td payload desc">
{!editable ? (
<CopyToClipboard text={item.description}>
<span>{item.description}</span>
</CopyToClipboard>
) : (
<SmartTextarea
value={item.description || ''}
onChange={(e: any) =>
handleChangePropertyField(
item.id,
'description',
e.target.value
)
}
rows="1"
className="form-control editable"
spellCheck={false}
placeholder=""
/>
)}
</div>
</div>
{item.children && item.children.length ? (
<SortableTreeTableRow
{...this.props}
property={item}
isExpanding={childrenIsExpanding}
/>
) : null}
</div>
)
})}
</div>
</RSortable>
)
)
}
}
class SortableTreeTable extends Component<any, any> {
render() {
const { root, editable } = this.props
return (
<div className={`SortableTreeTable ${editable ? 'editable' : ''}`}>
<SortableTreeTableHeader {...this.props} />
<SortableTreeTableRow {...this.props} property={root} />
<SortableTreeTableRow
{...this.props}
interfaceId={this.props.interfaceId}
property={root}
isExpanding={true}
/>
</div>
)
}
@ -312,11 +507,23 @@ class PropertyList extends Component<any, any> {
}
}
render() {
const { title, label, scope, properties = [], repository = {}, mod = {}, itf = {} } = this.props
if (!itf.id) { return null }
const {
title,
label,
scope,
properties = [],
repository = {},
mod = {},
itf = {},
} = this.props
if (!itf.id) {
return null
}
const { editable, requestParamsType } = this.props // itf.locker && (itf.locker.id === auth.id)
const pos = rptFromStr2Num(requestParamsType)
let scopedProperties = properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === scope)
let scopedProperties = properties
.map((property: any) => ({ ...property }))
.filter((property: any) => property.scope === scope)
if (scope === 'request' && editable) {
scopedProperties = scopedProperties.filter((s: any) => s.pos === pos)
}
@ -328,10 +535,17 @@ class PropertyList extends Component<any, any> {
<div className="toolbar">
<ButtonGroup size="small" color="primary">
{editable && [
<Button key={1} onClick={this.handleClickCreatePropertyButton}></Button>,
<Button key={2} onClick={this.handleClickImporterButton}></Button>,
<Button key={1} onClick={this.handleClickCreatePropertyButton}>
</Button>,
<Button key={2} onClick={this.handleClickImporterButton}>
</Button>,
]}
<Button className={this.state.previewer ? 'checked-button' : ''} onClick={this.handleClickPreviewerButton}>
<Button
className={this.state.previewer ? 'checked-button' : ''}
onClick={this.handleClickPreviewerButton}
>
</Button>
</ButtonGroup>
@ -340,42 +554,77 @@ class PropertyList extends Component<any, any> {
<div className="body">
<SortableTreeTable
root={Tree.arrayToTree(scopedProperties)}
interfaceId={itf.id}
editable={editable}
highlightId={this.state.highlightId}
// handlefocused={this.handlefocused}
handleClickCreateChildPropertyButton={this.handleClickCreateChildPropertyButton}
handleClickCreateChildPropertyButton={
this.handleClickCreateChildPropertyButton
}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
handleChangePropertyField={this.handleChangePropertyField}
handleSortProperties={this.handleSortProperties}
handleClickCreatePropertyButton={this.handleClickCreatePropertyButton}
handleClickCreatePropertyButton={
this.handleClickCreatePropertyButton
}
/>
</div>
<div className="footer">
{this.state.previewer && <Previewer scope={scope} label={label} properties={properties} itf={itf} />}
{this.state.previewer && (
<Previewer
scope={scope}
label={label}
properties={properties}
itf={itf}
/>
)}
</div>
<RModal
when={this.state.createProperty}
onClose={() => this.setState({ createProperty: false })}
onResolve={this.handleCreatePropertySucceeded}
>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} />
<PropertyForm
title={`新建${label}属性`}
scope={scope}
repository={repository}
mod={mod}
itf={itf}
/>
</RModal>
<RModal
when={!!this.state.createChildProperty}
onClose={() => this.setState({ createChildProperty: false })}
onResolve={this.handleCreatePropertySucceeded}
>
<PropertyForm title={`新建${label}属性`} scope={scope} repository={repository} mod={mod} itf={itf} parent={this.state.createChildProperty} />
<PropertyForm
title={`新建${label}属性`}
scope={scope}
repository={repository}
mod={mod}
itf={itf}
parent={this.state.createChildProperty}
/>
</RModal>
<RModal when={this.state.importer} onClose={() => this.setState({ importer: false })} onResolve={this.handleCreatePropertySucceeded}>
<Importer title={`导入${label}属性`} repository={repository} mod={mod} itf={itf} scope={scope} />
<RModal
when={this.state.importer}
onClose={() => this.setState({ importer: false })}
onResolve={this.handleCreatePropertySucceeded}
>
<Importer
title={`导入${label}属性`}
repository={repository}
mod={mod}
itf={itf}
scope={scope}
/>
</RModal>
</section>
)
}
handleClickCreatePropertyButton = () => {
this.handleClickCreateChildPropertyButton()
}
};
// handlefocused = () => {
// this.setState({ highlightId: undefined })
// }
@ -399,35 +648,37 @@ class PropertyList extends Component<any, any> {
handleAddMemoryProperty(child, () => {
/** empty */
})
}
};
handleClickImporterButton = () => {
this.setState({ importer: true })
}
};
handleClickPreviewerButton = () => {
this.setState({ previewer: !this.state.previewer })
}
};
handleChangePropertyField = (id: any, key: any, value: any) => {
const { handleChangeProperty } = this.props
const { properties } = this.props
const property = properties.find((property: any) => property.id === id)
handleChangeProperty({ ...property, [key]: value })
}
};
handleCreatePropertySucceeded = () => {
/** empty */
}
};
handleDeleteMemoryProperty = (e: any, property: any) => {
e.preventDefault()
const { handleDeleteMemoryProperty } = this.props
handleDeleteMemoryProperty(property)
}
};
handleSortProperties = (_: any, sortable: any) => {
const { properties } = this.props
const ids = sortable.toArray()
ids.forEach((id: any, index: any) => {
const property = properties.find((item: any) => item.id === id || item.id === +id)
const property = properties.find(
(item: any) => item.id === id || item.id === +id
)
property.priority = index + 1
})
}
};
}
export default PropertyList

@ -3,7 +3,7 @@
.RepositoryEditor
> .header
position: relative
padding: 2rem
padding: 2rem 2rem 1rem 2rem
background-color: #fafbfc
> .title
font-size: 2rem
@ -89,19 +89,7 @@
.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
@ -351,8 +339,8 @@
margin-bottom: -1px
.th, .td
&.operations
width: 4.5rem
min-width: 4.5rem
padding: .5rem
width: 1rem
&.name
width: 20rem
flex-grow: 3
@ -372,7 +360,6 @@
color: $brand
.td
&.operations
padding: .5rem .75rem
height: auto
line-height: 1
justify-content: flex-end
@ -390,6 +377,8 @@
&.payload.name
justify-content: space-between
position: relative
.name-wrapper
flex-grow: 1
@for $i from 1 through 42
&.depth-#{$i}
padding-left: $i * 1rem + 0.75rem
@ -416,7 +405,11 @@
&.payload.desc
word-break: break-word
.SortableTreeTable.editable
.flex-row
.flex-row
.th, .td
&.operations
width: 4.5rem
padding: .5rem .75rem
.td
&.payload
padding: 0

@ -8,11 +8,42 @@ import ModuleList from './ModuleList'
import InterfaceList from './InterfaceList'
import InterfaceEditor from './InterfaceEditor'
import DuplicatedInterfacesWarning from './DuplicatedInterfacesWarning'
import { addRepository, updateRepository, clearRepository, fetchRepository } 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, GoVersions, GoPlug, GoDatabase, GoJersey, GoLinkExternal } from 'react-icons/go'
import {
addRepository,
updateRepository,
clearRepository,
fetchRepository
} 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,
GoPlug,
GoDatabase,
GoJersey,
GoChecklist,
GoLinkExternal,
GoPencil
} from 'react-icons/go'
import './RepositoryEditor.css'
import ExportPostmanForm from '../repository/ExportPostmanForm'
@ -69,20 +100,34 @@ class RepositoryEditor extends Component<any, any> {
// }
}
render() {
const { location: { params }, auth } = this.props
const {
location: { params },
room,
auth,
} = this.props
let { repository } = this.props
if (!repository.fetching && !repository.data) { return <div className="p100 fontsize-30 text-center"></div> }
if (repository.fetching || !repository.data || !repository.data.id) { return <Spin /> }
if (!repository.fetching && !repository.data) {
return <div className="p100 fontsize-30 text-center"></div>
}
if (repository.fetching || !repository.data || !repository.data.id) {
return <Spin />
}
repository = repository.data
if (repository.name) {
document.title = `RAP2 ${repository.name}`
}
const mod = repository && repository.modules && repository.modules.length
? (repository.modules.find((item: any) => item.id === +params.mod) || repository.modules[0]) : {}
const itf = mod.interfaces && mod.interfaces.length
? (mod.interfaces.find((item: any) => item.id === +params.itf) || mod.interfaces[0]) : {}
const mod =
repository && repository.modules && repository.modules.length
? repository.modules.find((item: any) => item.id === +params.mod) ||
repository.modules[0]
: {}
const itf =
mod.interfaces && mod.interfaces.length
? mod.interfaces.find((item: any) => item.id === +params.itf) ||
mod.interfaces[0]
: {}
const properties = itf.properties || []
const ownerlink = repository.organization
@ -90,31 +135,87 @@ class RepositoryEditor extends Component<any, any> {
: `/repository/joined?user=${repository.owner.id}`
const isOwned = repository.owner.id === auth.id
const isJoined = repository.members.find((item: any) => item.id === auth.id)
const isJoined = repository.members.find(
(item: any) => item.id === auth.id
)
return (
<article className="RepositoryEditor">
<div className="header">
<span className="title">
<GoRepo className="mr6 color-9" />
<Link to={`${ownerlink}`}>{repository.organization ? repository.organization.name : repository.owner.fullname}</Link>
<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={() => this.setState({ update: true })}><GoPencil /> </span>
: null
}
<RModal when={this.state.update} onClose={() => this.setState({ update: false })} onResolve={this.handleUpdate}>
<RepositoryForm title="编辑仓库" repository={repository} />
{isOwned || isJoined ? (
<span
className="fake-link edit"
onClick={() => this.setState({ update: true })}
>
<GoPencil />
</span>
) : null}
<RModal when={this.state.update} onResolve={this.handleUpdate}>
<RepositoryForm
open={this.state.update}
onClose={ok => {
ok && this.handleUpdate()
this.setState({ update: false })
}}
title="编辑仓库"
repository={repository}
/>
</RModal>
<a href={`${serve}/app/plugin/${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoPlug /> </a>
<a href={`${serve}/repository/get?id=${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoDatabase /> </a>
<a href={`${serve}/test/test.plugin.jquery.html?id=${repository.id}`} target="_blank" rel="noopener noreferrer" className="api"><GoJersey /> </a>
<span className="fake-link edit" onClick={() => this.setState({ exportPostman: true })}><GoLinkExternal /> Postman Collection</span>
<a
href={`${serve}/app/plugin/${repository.id}`}
target="_blank"
rel="noopener noreferrer"
className="api"
>
<GoPlug />
</a>
<a
href={`${serve}/repository/get?id=${repository.id}`}
target="_blank"
rel="noopener noreferrer"
className="api"
>
<GoDatabase />
</a>
<a
href={`${serve}/test/test.plugin.jquery.html?id=${repository.id}`}
target="_blank"
rel="noopener noreferrer"
className="api"
>
<GoJersey />
</a>
{room &&
room[repository.id] &&
typeof room[repository.id].coverage !== 'undefined' && (
<a
href={`http://room.daily.taobao.net/index.html#/detail?projectId=${room[repository.id].roomProjectId}`}
target="_blank"
rel="noopener noreferrer"
>
<GoChecklist /> Room:{' '}
{Math.round(room[repository.id].coverage * 100)}%
</a>
)}
<span
className="fake-link edit"
onClick={() => this.setState({ exportPostman: true })}
>
<GoLinkExternal /> Postman Collection
</span>
<RModal
when={this.state.exportPostman}
onClose={() => this.setState({ exportPostman: false })}
@ -125,47 +226,36 @@ class RepositoryEditor extends Component<any, any> {
</div>
<RepositorySearcher repository={repository} />
<div className="desc">{repository.description}</div>
{this.renderRelatedProjects()}
<DuplicatedInterfacesWarning repository={repository} />
</div>
<div className="body">
<ModuleList mods={repository.modules} repository={repository} mod={mod} />
<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} />
<InterfaceList
itfs={mod.interfaces}
repository={repository}
mod={mod}
itf={itf}
/>
<InterfaceEditor
itf={itf}
properties={properties}
mod={mod}
repository={repository}
/>
</div>
</div>
</article>
)
}
renderRelatedProjects() {
const { repository } = this.props
const { collaborators } = repository.data
return (
<div className="RelatedProjects">
{collaborators &&
Array.isArray(collaborators) &&
collaborators.map(collab => (
<div
className="CollabProject Project"
key={`collab-${collab.id}`}
>
<span className="title">
<GoVersions className="mr5" />
</span>
<Link to={`/repository/editor?id=${collab.id}`}>
{collab.name}
</Link>
</div>
))}
</div>
)
}
handleUpdate = () => {
const { pathname, hash, search } = this.props.router.location
this.props.replace(pathname + search + hash)
}
};
}
// 容器组件
@ -174,7 +264,7 @@ const mapStateToProps = (state: RootState) => ({
repository: state.repository,
router: state.router,
})
const mapDispatchToProps = ({
const mapDispatchToProps = {
onFetchRepository: fetchRepository,
onAddRepository: addRepository,
onUpdateRepository: updateRepository,
@ -195,7 +285,7 @@ const mapDispatchToProps = ({
onDeleteProperty: deleteProperty,
onSortPropertyList: sortPropertyList,
replace,
})
}
export default connect(
mapStateToProps,
mapDispatchToProps

@ -29,10 +29,10 @@ const Home = ({ owned, joined, logs }: any) => {
return (
<div className="Home">
<div className="row">
<div className="col-12 col-sm-8 col-md-8 col-lg-8">
<div className="distribute-2-3 pl15 pr15">
<LogsCard logs={logs} />
</div>
<div className="col-12 col-sm-4 col-md-4 col-lg-4">
<div className="distribute-1-3 pl15 pl15">
<OwnedRepositoriesCard repositories={owned} />
<div style={{ marginTop: 8 }}>
<JoinedRepositoriesCard repositories={joined} />

@ -76,7 +76,7 @@ function OrganizationForm(props: Props) {
return (
<Dialog
open={open}
onClose={() => onClose()}
onClose={(_event, reason) => (reason !== 'backdropClick' && onClose())}
TransitionComponent={Transition}
>
<DialogTitle></DialogTitle>

@ -24,11 +24,12 @@
> .footer
.RepositoryList
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 20px;
padding: 0 15px;
.Repository.card
margin-bottom: 1rem;
// transition: box-shadow .15s ease-out;
// &:hover
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.18);
margin-bottom: 1rem;
.card-block
position: relative;
.name

@ -1,9 +1,15 @@
import React, { Component } from 'react'
import React, { useState, MouseEventHandler } from 'react'
import { connect, Link, replace, 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/go'
import {
GoRepo,
GoPencil,
GoPlug,
GoTrashcan,
GoPerson,
GoOrganization
} from 'react-icons/go'
import { RouterState } from 'connected-react-router'
import { RootState } from 'actions/types'
import { Card } from '@material-ui/core'
@ -19,76 +25,87 @@ interface Props {
replace: typeof replace
}
interface IStates {
update: boolean
}
function Repository(props: Props) {
const { auth, repository, editor, router } = props
const [open, setOpen] = useState(false)
class Repository extends Component<Props, IStates> {
constructor(props: any) {
super(props)
this.state = { update: false }
}
render() {
const { auth, repository, editor, router } = this.props
const { location } = router
return (
<Card 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">
<a href={`${serve}/app/plugin/${repository.id}`} target="_blank" rel="noopener noreferrer"><GoPlug /></a>
{/* 编辑权限:拥有者或者成员 */}
{repository.owner.id === auth.id || repository.members.find((item: any) => item.id === auth.id)
? <span className="fake-link" onClick={() => this.setState({ update: true })}><GoPencil /></span>
: null
}
<RModal when={this.state.update} onClose={() => this.setState({ update: false })} onResolve={this.handleUpdate}>
<RepositoryForm title="编辑仓库" repository={repository} />
</RModal>
{/* 删除权限:个人仓库 */}
{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>
</Card>
)
}
handleDeleteRepository = (e: any) => {
const handleDeleteRepository: MouseEventHandler<HTMLAnchorElement> = e => {
e.preventDefault()
const { repository, router, replace } = this.props
const message = `仓库被删除后不可恢复,并且会删除相关的模块和接口!\n确认继续删除『#${repository.id} ${repository.name}』吗?`
const { repository, router, replace } = props
const message = `仓库被删除后不可恢复,并且会删除相关的模块和接口!\n确认继续删除『#${
repository.id
} ${repository.name}`
if (window.confirm(message)) {
const { deleteRepository } = this.props
const { deleteRepository } = props
deleteRepository(repository.id)
const { pathname, hash, search } = router.location
replace(pathname + hash + search)
}
}
handleUpdate = () => {
const { pathname, hash, search } = this.props.router.location
this.props.replace(pathname + search + hash)
}
const { location } = router
return (
<Card 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">
<a
href={`${serve}/app/plugin/${repository.id}`}
target="_blank"
rel="noopener noreferrer"
>
<GoPlug />
</a>
{/* 编辑权限:拥有者或者成员 */}
{repository.owner.id === auth.id ||
repository.members.find((item: any) => item.id === auth.id) ? (
<span className="fake-link" onClick={() => setOpen(true)}>
<GoPencil />
</span>
) : null}
<RepositoryForm
title="编辑仓库"
open={open}
onClose={() => setOpen(false)}
repository={repository}
/>
{/* 删除权限:个人仓库 */}
{repository.owner.id === auth.id ? (
<Link
to={location.pathname + location.search}
onClick={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>
</Card>
)
}
// 容器组件
@ -96,10 +113,10 @@ const mapStateToProps = (state: RootState) => ({
auth: state.auth,
router: state.router,
})
const mapDispatchToProps = ({
const mapDispatchToProps = {
deleteRepository,
replace,
})
}
export default connect(
mapStateToProps,
mapDispatchToProps

@ -1,193 +1,229 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { mock } from 'mockjs'
import { SmartTextarea, MembersInput, RParsley } from '../utils'
import { GoInfo } from 'react-icons/go'
import { RootState } from 'actions/types'
import { Button } from '@material-ui/core'
import { updateRepository, addRepository } from 'actions/repository'
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { YUP_MSG } from '../../family/UIConst'
import { Formik, Field, Form } from 'formik'
import { TextField } from 'formik-material-ui'
import * as Yup from 'yup'
import { Button, Theme, Dialog, Slide, DialogContent, DialogTitle } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { Repository, RootState } from '../../actions/types'
import UserList from '../common/UserList'
import AccountService from '../../relatives/services/Account'
import * as _ from 'lodash'
import { updateRepository, addRepository } from '../../actions/repository'
import { refresh } from '../../actions/common'
// 模拟数据
// 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: [],
})
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
minHeight: 300,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
}))
class RepositoryForm extends Component<any, any> {
static contextTypes = {
rmodal: PropTypes.object.isRequired,
}
static propTypes = {
auth: PropTypes.object.isRequired,
organization: PropTypes.object,
repository: PropTypes.object,
}
rparsley: any
constructor(props: any) {
super(props)
const { repository } = props
this.state = repository ? {
...repository,
collaboratorIds: repository.collaborators.map((item: any) => item.id),
newOwner: repository.owner,
} : mockRepository()
const schema = Yup.object().shape<Partial<Repository>>({
name: Yup.string().required(YUP_MSG.REQUIRED).max(20, YUP_MSG.MAX_LENGTH(20)),
description: Yup.string().max(1000, YUP_MSG.MAX_LENGTH(1000)),
})
const FORM_STATE_INIT: Repository = {
id: 0,
name: '',
description: '',
members: [],
collaborators: [],
organizationid: 0,
collaboratorIdstring: '',
}
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />
})
interface Props {
title?: string
open: boolean
onClose: (isOk?: boolean) => void
repository?: Repository
}
function RepositoryForm(props: Props) {
const { open, onClose, repository, title } = props
if (repository) {
repository.collaboratorIdstring = repository.collaborators!.map(x => { return x.id }).join(',')
}
render() {
const { rmodal } = this.context
const { 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: any) => this.setState({ newOwner: users[0] })}
const auth = useSelector((state: RootState) => state.auth)
const classes = useStyles()
const dispatch = useDispatch()
return (
<Dialog
open={open}
onClose={(_event, reason) => (reason !== 'backdropClick' && onClose())}
TransitionComponent={Transition}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
...(repository || {}),
}}
validationSchema={schema}
onSubmit={(values) => {
const addOrUpdateRepository = values.id ? updateRepository : addRepository
const repository: Repository = {
...values,
memberIds: (values.members || []).map(
(user: any) => user.id
),
collaboratorIds: (
values.collaboratorIdstring || ''
).split(','),
}
const { owner, newOwner } = values
if (newOwner && newOwner.id !== owner!.id) { repository.ownerId = newOwner.id }
dispatch(addOrUpdateRepository(repository, () => {
dispatch(refresh())
onClose(true)
}))
}}
render={({ isSubmitting, setFieldValue, values }) => {
function loadUserOptions(input: string): Promise<Array<{ label: string, value: number }>> {
return new Promise(async (resolve) => {
const users = await AccountService.fetchUserList({ name: input })
const options = _.differenceWith(users.data, values.members || [], _.isEqual)
resolve(options.map(x => ({ label: `${x.fullname} ${x.empId || x.email}`, value: x.id })))
})
}
return (
<Form>
<div className="rmodal-body">
{values.id > 0 && (
<div className={classes.formItem}>
<div className={classes.formTitle}>
</div>
{values.owner &&
values.owner.id === auth.id ? (
<UserList
isMulti={false}
loadOptions={loadUserOptions}
selected={
values.owner
? [
{
label:
values.owner.fullname,
value: values.owner.id,
},
]
: []
}
onChange={(users: any) =>
setFieldValue('newOwner', users[0])
}
/>
) : (
<div className="pt7 pl9">
{values.owner!.fullname}
</div>
)}
</div>
)}
<div className={classes.formItem}>
<Field
name="name"
label="仓库名称"
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<Field
name="description"
label="简介"
multiline={true}
rows={3}
component={TextField}
fullWidth={true}
/>
</div>
<div className={classes.formItem}>
<div className={classes.formTitle}>
</div>
<UserList
isMulti={true}
loadOptions={loadUserOptions}
selected={values.members!.map(x => ({
label: x.fullname,
value: x.id,
}))}
onChange={selected =>
setFieldValue('members', selected)
}
/>
: <div className="pt7 pl9">{this.state.owner.fullname}</div>
}
</div>
<div className={classes.formItem}>
<Field
name="collaboratorIdstring"
label="协同仓库ID"
component={TextField}
fullWidth={true}
/>
</div>
</div>
</div>
}
{/* <div className='form-group row'>
<label className='col-sm-2 control-label'></label>
<div className='col-sm-10'>
<RadioList data={FORM.RADIO_LIST_DATA_VISIBILITY} curVal={this.state.visibility} name='visibility'
onChange={visibility => this.setState({ visibility })} />
</div>
</div> */}
<div className="form-group row">
<label className="col-sm-2 control-label"></label>
<div className="col-sm-10">
<input
name="name"
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
className="form-control"
placeholder="Name"
spellCheck={false}
autoComplete="off"
autoFocus={true}
required={true}
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: any) => 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: any) => 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" variant="contained" color="primary" style={{ marginRight: 8 }}></Button>
<Button onClick={e => { e.preventDefault(); rmodal.close() }} variant="contained"></Button>
</div>
</div>
</div>
</form>
</RParsley>
</section>
)
}
componentDidUpdate() {
this.context.rmodal.reposition()
}
handleSubmit = (e: any) => {
e.preventDefault()
if (!this.rparsley.isValid()) { return }
const { addRepository, updateRepository } = this.props
const onAddOrUpdateRepository = this.state.id ? updateRepository : addRepository
const { organization } = this.props
const repository: any = {
...this.state,
organizationId: organization ? organization.id : null,
memberIds: (this.state.members || []).map((user: any) => user.id),
}
const { owner, newOwner } = this.state
if (newOwner && newOwner.id !== owner.id) { repository.ownerId = newOwner.id }
const { rmodal } = this.context
rmodal.close()
onAddOrUpdateRepository(repository, () => {
if (rmodal) { rmodal.resolve() }
})
}
<div className={classes.ctl}>
<Button
type="submit"
variant="contained"
color="primary"
className="mr1"
disabled={isSubmitting}
>
</Button>
<Button
onClick={() => onClose()}
disabled={isSubmitting}
>
</Button>
</div>
</Form>
)
}}
/>
</div>
</DialogContent>
</Dialog>
)
}
// 容器组件
const mapStateToProps = (state: RootState) => ({
auth: state.auth,
})
const mapDispatchToProps = ({
updateRepository,
addRepository,
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(RepositoryForm)
export default RepositoryForm

@ -18,7 +18,7 @@ class RepositoryList extends Component<any, any> {
return (
<div className="RepositoryList row">
{repositories.map((repository: any) =>
<div key={repository.id} className="col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3">
<div key={repository.id} >
<Repository repository={repository} editor={editor} />
</div>
)}

@ -40,20 +40,40 @@ export function CreateButton(props: CreateButtonProps) {
return (
<span className="float-right ml10">
{/* DONE 2.1 √我加入的仓库、X所有仓库 是否应该有 新建仓库 */}
<Button className="RepositoryCreateButton" variant="contained" color="primary" onClick={() => setCreating(true)}> </Button>
{organization &&
<button className="RepositoryCreateButton btn btn-secondary ml8" onClick={() => setImporting(true)}>
<Button
className="RepositoryCreateButton"
variant="contained"
color="primary"
onClick={() => setCreating(true)}
>
{' '}
{' '}
</Button>
{organization && (
<button
className="RepositoryCreateButton btn btn-secondary ml8"
onClick={() => setImporting(true)}
>
<GoArrowRight />
</button>
}
<RModal when={creating} onClose={() => setCreating(false)} onResolve={handleUpdate}>
<RepositoryForm title="新建仓库" organization={organization} />
</RModal>
{organization &&
<RModal when={importing && !!organization} onClose={() => setImporting(false)} onResolve={handleUpdate}>
</button>
)}
<RepositoryForm
title="新建仓库"
open={creating}
onClose={() => setCreating(false)}
/>
{organization && (
<RModal
when={importing && !!organization}
onClose={() => setImporting(false)}
onResolve={handleUpdate}
>
<ImportRepositoryForm title="导入仓库" orgId={organization.id} />
</RModal>
}
)}
</span>
)
}

@ -1,3 +1,11 @@
@import "../../assets/variables.sass"
.rc-tooltip-content .rc-tooltip-inner
min-height: unset
padding: 0
.copy-link
cursor: pointer
color: #999
&:hover
color: $brand

@ -1,14 +1,16 @@
import copy from 'clipboard-copy'
import * as React from 'react'
import Tooltip from 'rc-tooltip'
import 'rc-tooltip/assets/bootstrap.css'
import FileCopy from '@material-ui/icons/FileCopyTwoTone'
import { withSnackbar, WithSnackbarProps } from 'notistack'
import 'rc-tooltip/assets/bootstrap.css'
import './CopyToClipboard.sass'
type Props = {
children: React.ReactElement<any>
text: string
children: React.ReactElement<any>;
text: string;
type?: 'hover' | 'right';
} & WithSnackbarProps
interface OwnState {
@ -19,13 +21,14 @@ class CopyToClipboard extends React.Component<Props, OwnState> {
public state: OwnState = { showTooltip: false }
public render() {
return (
const { type = 'hover', text } = this.props
return type === 'hover' ? (
<Tooltip
placement="right"
overlay={
<div
style={{ cursor: 'pointer', color: '#fff', padding: '8px 10px' }}
onClick={() => this.onCopy(this.props.text)}
onClick={() => this.onCopy(text)}
>
</div>
@ -37,29 +40,41 @@ class CopyToClipboard extends React.Component<Props, OwnState> {
>
{this.props.children}
</Tooltip>
) : (
<>
{this.props.children}
<span className="copy-link edit" onClick={() => this.onCopy(text)} title="复制名称">
<FileCopy fontSize="small"/>
</span>
</>
)
}
private onCopy = (content: string) => {
copy(content).then(() => {
const maxLength = 30
const cutContent = content.length > maxLength ? content.substr(0, maxLength) + '...' : content
this.props.enqueueSnackbar(`成功复制 ${cutContent} 到剪贴板`, {
variant: 'success',
autoHideDuration: 1000,
copy(content)
.then(() => {
const maxLength = 30
const cutContent =
content.length > maxLength
? content.substr(0, maxLength) + '...'
: content
this.props.enqueueSnackbar(`成功复制 ${cutContent} 到剪贴板`, {
variant: 'success',
autoHideDuration: 1000,
})
})
}).catch(() => {
this.props.enqueueSnackbar(`复制失败`, {
variant: 'error',
autoHideDuration: 1000,
.catch(() => {
this.props.enqueueSnackbar(`复制失败`, {
variant: 'error',
autoHideDuration: 1000,
})
})
})
this.setState({ showTooltip: false })
};
private handleVisibleChange = (visible: boolean) => {
this.setState({showTooltip: visible})
}
this.setState({ showTooltip: visible })
};
}
export default withSnackbar<Props>(CopyToClipboard)

@ -1,5 +1,4 @@
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import './assets/index.css'
import 'animate.css'

@ -84,7 +84,7 @@ export function* unlockInterface(action: any) {
yield put(InterfaceAction.unlockInterfaceSucceeded(action.id))
if (action.onResolved) { action.onResolved() }
} else {
alert(`发生错误:${res.errMsg}`)
window.alert(`发生错误:${res.errMsg}`)
}
} catch (e) {
console.error(e.message)

@ -64,7 +64,9 @@ const Routes = () => {
return (
<article className="Routes">
<Message messageInfo={message} />
<div className="btn-top" onClick={() => { console.log('hahaha'); window.scrollTo(0, 0) }}> </div>
<div className="btn-top" onClick={() => { console.log('hahaha'); window.scrollTo(0, 0) }}>
</div>
<Route component={Header} />
<div className="body">
<Suspense fallback={<Spin/>}>

Loading…
Cancel
Save