feat: 模块移动、swagger 导入

pull/103/head
bigfengyu 5 years ago
parent dc34d5cecd
commit 4394f206ef

@ -28,6 +28,7 @@
"@material-ui/pickers": "^3.2.6",
"@material-ui/styles": "^4.4.3",
"@types/json5": "^0.0.30",
"@welldone-software/why-did-you-render": "^4.0.3",
"animate.css": "3.7.2",
"awesome-debounce-promise": "^2.1.0",
"chart.js": "^2.8.0",

@ -26,6 +26,20 @@ export const updateModuleFailed = (message: any) => ({
message,
})
export const moveModule = (params: any, onResolved: any) => ({
type: 'MODULE_MOVE',
params,
onResolved,
})
export const moveModuleSucceeded = (payload: any) => ({
type: 'MODULE_MOVE_SUCCEEDED',
payload,
})
export const moveModuleFailed = (message: any) => ({
type: 'MODULE_MOVE_FAILED',
message,
})
export const deleteModule = (id: any, onResolved: any, repoId: any) => ({
type: 'MODULE_DELETE',
id,

@ -8,6 +8,10 @@ export const importRepository = (data: any, onResolved: any) => ({ type: 'REPOSI
export const importRepositorySucceeded = () => ({ type: 'REPOSITORY_IMPORT_SUCCEEDED' })
export const importRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_IMPORT_FAILED', message })
export const importSwaggerRepository = (data: any, onResolved: any) => ({ type: 'REPOSITORY_IMPORT_SWAGGER', onResolved, data })
export const importSwaggerRepositorySucceeded = () => ({ type: 'REPOSITORY_IMPORT_SUCCEEDED_SWAGGER' })
export const importSwaggerRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_IMPORT_FAILED_SWAGGER', message })
export const updateRepository = (repository: any, onResolved: any) => ({ type: 'REPOSITORY_UPDATE', repository, onResolved })
export const updateRepositorySucceeded = (repository: any) => ({ type: 'REPOSITORY_UPDATE_SUCCEEDED', repository })
export const updateRepositoryFailed = (message: any) => ({ type: 'REPOSITORY_UPDATE_FAILED', message })

@ -44,6 +44,14 @@ export interface Organization {
newOwner?: User
}
export interface ImportSwagger {
version: number
docUrl: string
orgId?: number
mode: string
repositoryId?: number
swagger?: string
}
export interface User {
id: number
fullname: string

File diff suppressed because one or more lines are too long

@ -193,4 +193,10 @@ code
&.alert-info
color: #0c5460
background-color: #d1ecf1
border-color: #bee5eb
border-color: #bee5eb
.mt10
margin-top: 10px
.mt20
margin-top: 20px

@ -6,6 +6,3 @@
font-size: 2rem
.toolbar
margin-bottom: 1rem
.body
.table
.footer

@ -0,0 +1,36 @@
import React from 'react'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
interface Props {
open: boolean
title?: string
content: React.ReactNode
type: 'alert' | 'confirm'
onConfirm: () => void
onCancel: () => void
}
export default function ConfirmDialog(props: Props) {
const { type, title = '确认' } = props
return (
<Dialog open={props.open} onClose={props.onCancel}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{props.content}</DialogContentText>
</DialogContent>
<DialogActions>
{type === 'confirm' && (
<Button onClick={props.onCancel} variant="outlined" color="default">
</Button>
)}
<Button onClick={props.onConfirm} variant="outlined" color="primary" autoFocus={true}>
</Button>
</DialogActions>
</Dialog>
)
}

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["Footer.sass"],"names":[],"mappings":"AAEA;EACE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AACA;EACE;EACA","file":"Footer.css"}
{"version":3,"sourceRoot":"","sources":["Footer.sass"],"names":[],"mappings":"AAEA;EACE;EACA;EACA;;AACA;EACE;EACA;EACA;;AACA;EACE;EACA","file":"Footer.css"}

@ -1,7 +1,6 @@
@import "../../assets/variables.sass"
.Footer
margin-top: 2rem
padding: 2rem 1rem 1rem 1rem
border-top: 1px solid $border
text-align: center

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

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

@ -54,7 +54,7 @@ function DefaultValueModal({ open, handleClose, repositoryId, enqueueSnackbar }:
useEffect(() => {
dispatch(fetchDefaultVals(repositoryId))
}, [repositoryId])
}, [dispatch, repositoryId])
const defaultVals: IDefaultVal[] = useSelector((state: any) => state.defaultVals)
useEffect(() => {

@ -76,6 +76,7 @@ type ImporterProps = {
rmodal?: any,
title?: any,
handleAddMemoryProperties: (...args: any[]) => any,
interfaceId: number
[k: string]: any;
}
type ImporterState = {
@ -143,7 +144,7 @@ class Importer extends Component<ImporterProps, ImporterState> {
// DONE 2.1 BUG 类型 Number初始值 '',被解析为随机字符串
handleJSONSchema = (schema: any, parent = { id: -1 }, memoryProperties: any, siblings?: any) => {
if (!schema) { return }
const { auth, repository, mod, itf, scope } = this.props
const { auth, repository, mod, interfaceId, scope } = this.props
const hasSiblings = siblings instanceof Array && siblings.length > 0
// DONE 2.1 需要与 Mock 的 rule.type 规则统一,首字符小写,好烦!应该忽略大小写!
if (schema.name === lengthAlias) {
@ -207,7 +208,7 @@ class Importer extends Component<ImporterProps, ImporterState> {
creator: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
interfaceId,
scope,
parentId: parent.id,
},

@ -97,9 +97,6 @@ class InterfaceEditor extends Component<
const prevStates = this.state
this.setState(InterfaceEditor.mapPropsToState(nextProps, prevStates))
}
// Use shouldComponentUpdate() to let React know if a component's output is not affected by the current change in state or props.
// TODO 2.2
// shouldComponentUpdate (nextProps, nextState) {}
render() {
const { auth, repository, mod } = this.props
@ -119,9 +116,7 @@ class InterfaceEditor extends Component<
moveInterface={this.handleMoveInterface}
handleLockInterface={this.handleLockInterface}
handleMoveInterface={this.handleMoveInterface}
handleSaveInterfaceAndProperties={
this.handleSaveInterfaceAndProperties
}
handleSaveInterfaceAndProperties={this.handleSaveInterfaceAndProperties}
handleUnlockInterface={this.handleUnlockInterface}
/>
<InterfaceSummary
@ -139,7 +134,7 @@ class InterfaceEditor extends Component<
editable={editable}
repository={repository}
mod={mod}
itf={this.state.itf}
interfaceId={itf.id}
bodyOption={this.state.summaryState.bodyOption}
requestParamsType={this.state.summaryState.requestParamsType}
handleChangeProperty={this.handleChangeProperty}
@ -151,19 +146,21 @@ class InterfaceEditor extends Component<
editable={editable}
repository={repository}
mod={mod}
itf={this.state.itf}
interfaceId={itf.id}
handleChangeProperty={this.handleChangeProperty}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
/>
{this.state.moveInterfaceDialogOpen && <MoveInterfaceForm
title="移动/复制接口"
mod={mod}
repository={repository}
itfId={itf.id}
open={this.state.moveInterfaceDialogOpen}
onClose={() => this.setState({ moveInterfaceDialogOpen: false })}
/>}
{this.state.moveInterfaceDialogOpen && (
<MoveInterfaceForm
title="移动/复制接口"
mod={mod}
repository={repository}
itfId={itf.id}
open={this.state.moveInterfaceDialogOpen}
onClose={() => this.setState({ moveInterfaceDialogOpen: false })}
/>
)}
</article>
)
}

@ -102,7 +102,6 @@ function InterfaceForm(props: Props) {
repositoryId: repository!.id,
moduleId: mod!.id,
}
console.log('itf', itf)
dispatch(
addOrUpdateInterface(itf, () => {
dispatch(refresh())

@ -1,26 +1,19 @@
import React, { useState, MouseEventHandler } from 'react'
import {
connect,
Link,
StoreStateRouterLocationURI,
replace
} from '../../family'
import { connect, Link, StoreStateRouterLocationURI, replace } from '../../family'
import { sortInterfaceList, deleteInterface } from '../../actions/interface'
import { deleteModule } from '../../actions/module'
import { Module, Repository, RootState, Interface, User } from '../../actions/types'
import { refresh } from '../../actions/common'
import { RSortable } from '../utils'
import InterfaceForm from './InterfaceForm'
import { useConfirm } from 'hooks/useConfirm'
import { GoPencil, GoTrashcan, GoLock } from 'react-icons/go'
import { getCurrentInterface } from '../../selectors/interface'
import Button from '@material-ui/core/Button'
import { Button, ButtonGroup } from '@material-ui/core/'
import ModuleForm from './ModuleForm'
import MoveModuleForm from './MoveModuleForm'
import { useSelector, useDispatch } from 'react-redux'
import './InterfaceList.css'
import {
Module,
Repository,
RootState,
Interface,
User
} from '../../actions/types'
import { refresh } from '../../actions/common'
interface InterfaceBaseProps {
repository: Repository
@ -47,9 +40,7 @@ function InterfaceBase(props: InterfaceBaseProps) {
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 { deleteInterface } = props
deleteInterface(props.itf!.id, () => {
@ -69,11 +60,15 @@ function InterfaceBase(props: InterfaceBaseProps) {
if (
curItf &&
curItf.locker &&
!window.confirm(
'编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?'
)
!window.confirm('编辑模式下切换接口,会导致编辑中的资料丢失,是否确定切换接口?')
) {
e.preventDefault()
} else {
const top = document.querySelector<HTMLElement>('.InterfaceEditor')!.offsetTop - 10
// 当接口列表悬浮时切换接口自动跳转到接口顶部
if (window.scrollY > top) {
window.scrollTo(0, top)
}
}
}}
>
@ -119,10 +114,7 @@ const mapDispatchToProps = {
replace,
deleteInterface,
}
const InterfaceWrap = connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceBase)
const InterfaceWrap = connect(mapStateToProps, mapDispatchToProps)(InterfaceBase)
interface InterfaceListProps {
itfs?: Interface[]
@ -132,36 +124,106 @@ interface InterfaceListProps {
repository: Repository
}
function InterfaceList(props: InterfaceListProps) {
const [open, setOpen] = useState(false)
const [interfaceFormOpen, setInterfaceFormOpen] = useState(false)
const [moduleFormOpen, setModuleFormOpen] = useState(false)
const [moveModuleFormOpen, setMoveModuleFormOpen] = useState(false)
const dispatch = useDispatch()
const confirm = useConfirm()
const auth = useSelector((state: RootState) => state.auth)
const router = useSelector((state: RootState) => state.router)
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 handleDeleteModule: MouseEventHandler<HTMLButtonElement> = e => {
e.preventDefault()
const message = (
<div>
<div></div>
<div>
#${mod.id} ${mod.name}
</div>
</div>
)
confirm({
title: '确认删除模块',
content: message,
}).then(() => {
dispatch(
deleteModule(
props.mod.id,
() => {
dispatch(refresh())
},
repository!.id,
),
)
const { pathname, hash, search } = router.location
dispatch(replace(pathname + hash + search))
})
}
const handleSort = (_: any, sortable: any) => {
dispatch(sortInterfaceList(sortable.toArray(), mod.id, () => {
/** empty */
}))
dispatch(
sortInterfaceList(sortable.toArray(), mod.id, () => {
/** empty */
}),
)
}
return (
<article className="InterfaceList">
{isOwned || isJoined ? (
<div className="header">
<Button
className="newIntf"
variant="outlined"
fullWidth={true}
color="primary"
onClick={() => setOpen(true)}
onClick={() => setInterfaceFormOpen(true)}
>
</Button>
<InterfaceForm
title="新建接口"
repository={repository}
mod={mod}
open={open}
onClose={() => setOpen(false)}
open={interfaceFormOpen}
onClose={() => setInterfaceFormOpen(false)}
/>
<ButtonGroup fullWidth={true} size="small">
<Button variant="outlined" color="primary" onClick={() => setModuleFormOpen(true)}>
</Button>
<Button variant="outlined" color="primary" onClick={() => setMoveModuleFormOpen(true)}>
/
</Button>
<Button variant="outlined" color="primary" onClick={handleDeleteModule}>
</Button>
</ButtonGroup>
{moduleFormOpen && (
<ModuleForm
title="修改模块"
module={mod}
repository={repository}
open={moduleFormOpen}
onClose={() => setModuleFormOpen(false)}
/>
)}
{moveModuleFormOpen && (
<MoveModuleForm
title="移动/复制模块"
mod={mod}
repository={repository}
open={moveModuleFormOpen}
onClose={() => setMoveModuleFormOpen(false)}
/>
)}
</div>
) : null}
{itfs.length ? (
@ -179,7 +241,6 @@ function InterfaceList(props: InterfaceListProps) {
itf={item}
active={item.id === itf!.id}
auth={auth}
// curItf={curItf}
/>
</li>
))}
@ -192,7 +253,4 @@ function InterfaceList(props: InterfaceListProps) {
)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InterfaceList)
export default connect(mapStateToProps, mapDispatchToProps)(InterfaceList)

@ -12,7 +12,7 @@ class Previewer extends Component<any, any> {
label: PropTypes.string.isRequired,
scope: PropTypes.string.isRequired,
properties: PropTypes.array.isRequired,
itf: PropTypes.object.isRequired,
interfaceId: PropTypes.number.isRequired,
}
render() {
let scopedTemplate
@ -20,13 +20,17 @@ class Previewer extends Component<any, any> {
let scopedData: any
let scopedKeys
let extraKeys
const { label, scope, properties, itf } = this.props
const { label, scope, properties, interfaceId } = this.props
try {
// DONE 2.2 支持引用请求参数
scopedProperties = {
request: properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === 'request'),
response: properties.map((property: any) => ({ ...property })).filter((property: any) => property.scope === 'response'),
request: properties
.map((property: any) => ({ ...property }))
.filter((property: any) => property.scope === 'request'),
response: properties
.map((property: any) => ({ ...property }))
.filter((property: any) => property.scope === 'response'),
}
scopedTemplate = {
request: Tree.treeToJson(Tree.arrayToTree(scopedProperties.request)),
@ -41,7 +45,7 @@ class Previewer extends Component<any, any> {
request: Mock.mock(scopedTemplate.request),
}
scopedData.response = Mock.mock(
Object.assign({}, _.pick(scopedData.request, extraKeys), scopedTemplate.response)
Object.assign({}, _.pick(scopedData.request, extraKeys), scopedTemplate.response),
)
scopedData.response = _.pick(scopedData.response, scopedKeys.response)
@ -53,11 +57,13 @@ class Previewer extends Component<any, any> {
// DONE 2.1 支持虚拟属性 __root__ √服务端 √前端 √迁移测试
const keys = Object.keys(data)
if (keys.length === 1 && keys[0] === '__root__') { data = data.__root__ }
if (keys.length === 1 && keys[0] === '__root__') {
data = data.__root__
}
const { Assert } = Mock.valid
const valid = Mock.valid(template, data)
for (const i of valid) {
for (const i of valid) {
console.warn(Assert.message(i))
}
return (
@ -65,37 +71,69 @@ class Previewer extends Component<any, any> {
<div className="result-template">
<div className="header">
<span className="title">{label}</span>
{scope === 'response'
? <a href={`${serve}/app/mock/template/${itf.id}`} target="_blank" rel="noopener noreferrer"><GoLink className="fontsize-14" /></a>
: null}
{scope === 'response' ? (
<a
href={`${serve}/app/mock/template/${interfaceId}`}
target="_blank"
rel="noopener noreferrer"
>
<GoLink className="fontsize-14" />
</a>
) : null}
</div>
<pre className="body">{
JSON.stringify(template, (_: any, v) => {
if (typeof v === 'function') { return v.toString() }
if (v !== undefined && v !== null && v.exec) { return v.toString() } else { return v }
}, 2)
}</pre>
<pre className="body">
{JSON.stringify(
template,
(_: any, v) => {
if (typeof v === 'function') {
return v.toString()
}
if (v !== undefined && v !== null && v.exec) {
return v.toString()
} else {
return v
}
},
2,
)}
</pre>
</div>
<div className="result-mocked">
<div className="header">
<span className="title">{label}</span>
{scope === 'response'
? <a href={`${serve}/app/mock/data/${itf.id}`} target="_blank" rel="noopener noreferrer"><GoLink className="mr6 fontsize-14" /></a>
: null}
<Link to="" onClick={e => this.remock(e)}><GoSync className="mr6 fontsize-14" onAnimationEnd={e => this.removeAnimateClass(e)} /></Link>
{scope === 'response' ? (
<a
href={`${serve}/app/mock/data/${interfaceId}`}
target="_blank"
rel="noopener noreferrer"
>
<GoLink className="mr6 fontsize-14" />
</a>
) : null}
<Link to="" onClick={e => this.remock(e)}>
<GoSync
className="mr6 fontsize-14"
onAnimationEnd={e => this.removeAnimateClass(e)}
/>
</Link>
</div>
<pre className="body">{JSON.stringify(data, null, 2)}</pre>
</div>
{scope === 'response'
? <div className="result-valid col-12">
{!valid.length
? <span><GoBeaker className="mr6 fontsize-20" /> </span>
: <span><GoBug className="mr6 fontsize-20" /></span>
}
{scope === 'response' ? (
<div className="result-valid col-12">
{!valid.length ? (
<span>
<GoBeaker className="mr6 fontsize-20" />
</span>
) : (
<span>
<GoBug className="mr6 fontsize-20" />
</span>
)}
</div>
: null
}
) : null}
</div>
)
} catch (ex) {

@ -217,7 +217,7 @@ class InterfaceSummary extends Component<
style={{ marginTop: 0 }}
id="name"
label="名称"
value={itf.name}
value={itf.name || ''}
fullWidth={true}
autoComplete="off"
onChange={e => {
@ -230,7 +230,7 @@ class InterfaceSummary extends Component<
<TextField
id="url"
label="地址"
value={itf.url}
value={itf.url || ''}
fullWidth={true}
autoComplete="off"
onChange={e => {
@ -246,9 +246,7 @@ class InterfaceSummary extends Component<
</InputLabel>
<Select
value={itf.method}
input={
<Input name="method" id="method-label-placeholder" />
}
input={<Input name="method" id="method-label-placeholder" />}
onChange={e => {
handleChangeInterface({ method: e.target.value })
}}
@ -268,9 +266,7 @@ class InterfaceSummary extends Component<
</InputLabel>
<Select
value={itf.status}
input={
<Input name="status" id="status-label-placeholder" />
}
input={<Input name="status" id="status-label-placeholder" />}
onChange={e => {
handleChangeInterface({ status: e.target.value })
}}
@ -289,7 +285,7 @@ class InterfaceSummary extends Component<
<TextField
id="description"
label="描述(可多行)"
value={itf.description}
value={itf.description || ''}
fullWidth={true}
multiline={true}
autoComplete="off"
@ -303,13 +299,11 @@ class InterfaceSummary extends Component<
) : (
<>
<li>
<CopyToClipboard text={itf.url}>
<span>
<CopyToClipboard text={itf.url} type="right">
<span className="mr5">
<span className="label"></span>
<a
href={`${serve}/app/mock/${repository.id}${getRelativeUrl(
itf.url || ''
)}`}
href={`${serve}/app/mock/${repository.id}${getRelativeUrl(itf.url || '')}`}
target="_blank"
rel="noopener noreferrer"
>
@ -338,8 +332,12 @@ class InterfaceSummary extends Component<
<li>
<CopyToClipboard text={itf.description}>
<span>
<span className="label"></span>
<span>{itf.description}</span>
<span className="label" style={{ verticalAlign: 'top' }}>
</span>
<span style={{ whiteSpace: 'pre-wrap', display: 'inline-block' }}>
{itf.description}
</span>
</span>
</CopyToClipboard>
</li>
@ -351,15 +349,11 @@ class InterfaceSummary extends Component<
<ul className="nav nav-tabs" role="tablist">
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.HEADERS
)}
onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.HEADERS)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.HEADERS
? 'active'
: ''
requestParamsType === REQUEST_PARAMS_TYPE.HEADERS ? 'active' : ''
}`}
role="tab"
data-toggle="tab"
@ -369,15 +363,11 @@ class InterfaceSummary extends Component<
</li>
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.QUERY_PARAMS
)}
onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.QUERY_PARAMS)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS
? 'active'
: ''
requestParamsType === REQUEST_PARAMS_TYPE.QUERY_PARAMS ? 'active' : ''
}`}
role="tab"
data-toggle="tab"
@ -387,15 +377,11 @@ class InterfaceSummary extends Component<
</li>
<li
className="nav-item"
onClick={this.switchRequestParamsType(
REQUEST_PARAMS_TYPE.BODY_PARAMS
)}
onClick={this.switchRequestParamsType(REQUEST_PARAMS_TYPE.BODY_PARAMS)}
>
<button
className={`nav-link ${
requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS
? 'active'
: ''
requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? 'active' : ''
}`}
role="tab"
data-toggle="tab"
@ -407,10 +393,7 @@ class InterfaceSummary extends Component<
)}
{editable && requestParamsType === REQUEST_PARAMS_TYPE.BODY_PARAMS ? (
<div className="body-options">
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}
>
<div className="form-check" onClick={this.switchBodyOption(BODY_OPTION.FORM_DATA)}>
<input
className="form-check-input"
type="radio"
@ -437,10 +420,7 @@ class InterfaceSummary extends Component<
x-www-form-urlencoded
</label>
</div>
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.RAW)}
>
<div className="form-check" onClick={this.switchBodyOption(BODY_OPTION.RAW)}>
<input
className="form-check-input"
type="radio"
@ -452,10 +432,7 @@ class InterfaceSummary extends Component<
raw
</label>
</div>
<div
className="form-check"
onClick={this.switchBodyOption(BODY_OPTION.BINARY)}
>
<div className="form-check" onClick={this.switchBodyOption(BODY_OPTION.BINARY)}>
<input
className="form-check-input"
type="radio"

@ -3,10 +3,9 @@ import { connect, Link, replace, StoreStateRouterLocationURI } from '../../famil
import { RSortable } from '../utils'
import ModuleForm from './ModuleForm'
import { useSelector, useDispatch } from 'react-redux'
import { GoPencil, GoTrashcan, GoPackage } from 'react-icons/go'
import { GoPackage } from 'react-icons/go'
import { deleteModule, sortModuleList } from '../../actions/module'
import { Module, Repository, RootState, User } from '../../actions/types'
import { refresh } from '../../actions/common'
interface ModuleBaseProps {
repository: Repository
@ -17,54 +16,16 @@ interface ModuleBaseProps {
replace?: typeof replace
}
function ModuleBase(props: ModuleBaseProps) {
const { repository, mod} = props
const auth = useSelector((state: RootState) => state.auth)
const { mod} = props
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)) {
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>
)
}

@ -1,6 +1,10 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { moveInterface } from '../../actions/interface'
import { fetchOwnedRepositoryList, fetchJoinedRepositoryList } from '../../actions/repository'
import EditorService from 'relatives/services/Editor'
import { Dialog, DialogTitle, DialogContent } from '@material-ui/core'
import _ from 'lodash'
import {
Button,
Select,
@ -10,10 +14,10 @@ import {
FormControlLabel,
Radio,
Theme,
makeStyles
makeStyles,
} from '@material-ui/core'
import { useDispatch } from 'react-redux'
import { Module } from 'actions/types'
import { useSelector, useDispatch } from 'react-redux'
import { Module, RootState } from 'actions/types'
export const OP_MOVE = 1
export const OP_COPY = 2
@ -43,6 +47,9 @@ const useStyles = makeStyles(({ spacing }: Theme) => ({
ctl: {
marginTop: spacing(2),
},
select: {
width: spacing(20),
},
}))
interface Props {
@ -54,53 +61,90 @@ interface Props {
onClose: () => void
}
// constructor(props: any) {
// super(props)
// const { repository } = props
// this.state = { modId, op: OP_MOVE }
// }
export default function MoveInterfaceForm(props: Props) {
const { repository, title, itfId, onClose, open, mod } = props
const classes = useStyles()
const [repositoryId, setRepositoryId] = useState(repository.id)
const [modId, setModId] = useState(mod.id)
const [op, setOp] = useState(OP_MOVE)
const [modules, setModules] = useState(repository.modules)
const dispatch = useDispatch()
const repositories = useSelector((state: RootState) => {
return _.uniqBy([...state.ownedRepositories.data, ...state.joinedRepositories.data], 'id')
})
useEffect(() => {
if (!repositories.length) {
dispatch(fetchJoinedRepositoryList())
dispatch(fetchOwnedRepositoryList())
}
}, [])
function onRepositoryChange(
e: React.ChangeEvent<{
name?: string | undefined
value: unknown
}>,
) {
const repositoryId = e.target.value
setRepositoryId(repositoryId)
EditorService.fetchModuleList({
repositoryId,
}).then(res => {
setModules(res)
setModId(res[0] && res[0].id)
})
}
const handleSubmit = (e: any) => {
e.preventDefault()
const params = {
modId,
op,
itfId,
repoId: repository.id,
repositoryId,
}
dispatch(
moveInterface(params, () => {
onClose()
})
}),
)
}
return (
<Dialog
open={open}
onClose={(_event, reason) => reason !== 'backdropClick' && onClose()}
>
<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
className={classes.select}
onChange={onRepositoryChange}
value={repositoryId}
fullWidth={true}
>
{repositories.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>
<FormControl>
<Select
className={classes.select}
onChange={e => setModId(+((e.target.value as any) as string))}
value={modId}
fullWidth={true}
>
{repository.modules.map((x: any) => (
{modules.map((x: any) => (
<MenuItem key={x.id} value={x.id}>
{x.name}
</MenuItem>
@ -109,7 +153,7 @@ export default function MoveInterfaceForm(props: Props) {
</FormControl>
</div>
<div className={classes.formItem}>
<div className={classes.formTitle}></div>
<div className={classes.formTitle}></div>
<RadioGroup
name="radioListOp"
value={String(op)}
@ -118,25 +162,12 @@ export default function MoveInterfaceForm(props: Props) {
}}
row={true}
>
<FormControlLabel
value={String(OP_COPY)}
control={<Radio />}
label="复制"
/>
<FormControlLabel
value={String(OP_MOVE)}
control={<Radio />}
label="移动"
/>
<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 type="submit" variant="contained" color="primary" style={{ marginRight: 8 }}>
</Button>
<Button onClick={() => onClose()}></Button>

@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react'
import { moveModule } from '../../actions/module'
import { fetchOwnedRepositoryList, fetchJoinedRepositoryList } from '../../actions/repository'
import { Dialog, DialogTitle, DialogContent } from '@material-ui/core'
import _ from 'lodash'
import {
Button,
Select,
MenuItem,
FormControl,
RadioGroup,
FormControlLabel,
Radio,
Theme,
makeStyles,
} from '@material-ui/core'
import { useSelector, useDispatch } from 'react-redux'
import { Module, RootState } from 'actions/types'
export const OP_MOVE = 1
export const OP_COPY = 2
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {},
appBar: {
position: 'relative',
},
title: {
marginLeft: spacing(2),
flex: 1,
},
preview: {
marginTop: spacing(1),
},
form: {
minWidth: 500,
},
formTitle: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: 9,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(2),
},
select: {
width: spacing(20),
},
}))
interface Props {
title: string
repository: any
open: boolean
mod: Module
onClose: () => void
}
export default function MoveModuleForm(props: Props) {
const { repository, title, onClose, open, mod } = props
const modId = mod.id
const classes = useStyles()
const [repositoryId, setRepositoryId] = useState(repository.id)
const [op, setOp] = useState(OP_MOVE)
const dispatch = useDispatch()
const repositories = useSelector((state: RootState) => {
return _.uniqBy([...state.ownedRepositories.data, ...state.joinedRepositories.data], 'id')
})
useEffect(() => {
if (!repositories.length) {
dispatch(fetchJoinedRepositoryList())
dispatch(fetchOwnedRepositoryList())
}
}, [])
function onRepositoryChange(
e: React.ChangeEvent<{
name?: string | undefined
value: unknown
}>,
) {
const repositoryId = e.target.value
setRepositoryId(repositoryId)
}
const handleSubmit = (e: any) => {
e.preventDefault()
const params = {
modId,
op,
repositoryId,
}
dispatch(
moveModule(params, () => {
onClose()
}),
)
}
return (
<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
className={classes.select}
onChange={onRepositoryChange}
value={repositoryId}
fullWidth={true}
>
{repositories.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>
</form>
</DialogContent>
</Dialog>
)
}

@ -37,7 +37,7 @@ class PropertyForm extends Component<any, any> {
parent: PropTypes.object,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
interfaceId: PropTypes.number.isRequired,
}
static contextTypes = {
rmodal: PropTypes.instanceOf(Component),
@ -54,7 +54,7 @@ class PropertyForm extends Component<any, any> {
<div className="rmodal-header">
<span className="rmodal-title">{this.props.title}</span>
</div>
<form className="form-horizontal w600" onSubmit={this.handleSubmit} >
<form className="form-horizontal w600" onSubmit={this.handleSubmit}>
<div className="rmodal-body">
<div className="form-group row" style={{}}>
<label className="col-sm-2 control-label"></label>
@ -88,9 +88,11 @@ class PropertyForm extends Component<any, any> {
}}
className="form-control"
>
{TYPES.map(type =>
<option key={type} value={type}>{type}</option>
)}
{TYPES.map(type => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
</div>
@ -142,7 +144,14 @@ class PropertyForm extends Component<any, any> {
<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
type="submit"
variant="contained"
color="primary"
style={{ marginRight: 8 }}
>
</Button>
<Button onClick={() => rmodal.close()}></Button>
</div>
</div>
@ -156,19 +165,21 @@ class PropertyForm extends Component<any, any> {
}
handleSubmit = (e: any) => {
e.preventDefault()
const { auth, repository, mod, itf, scope, parent = { id: -1 } } = this.props
const { auth, repository, mod, interfaceId, scope, parent = { id: -1 } } = this.props
const { handleAddMemoryProperty } = this.context
const property = Object.assign({}, this.state, {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
interfaceId,
scope,
parentId: parent.id,
})
handleAddMemoryProperty(property, () => {
const { rmodal } = this.context
if (rmodal) { rmodal.resolve() }
if (rmodal) {
rmodal.resolve()
}
})
}
}

@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React, { Component, PureComponent } from 'react'
import { PropTypes, Link } from '../../family'
import {
Tree,
@ -152,10 +152,9 @@ interface SortableTreeTableRowProps {
interfaceId: number
[k: string]: any
}
class SortableTreeTableRow extends Component<
SortableTreeTableRowProps,
SortableTreeTableRowState
> {
class SortableTreeTableRow extends Component<SortableTreeTableRowProps, SortableTreeTableRowState> {
static displayName = 'SortableTreeTableRow'
static whyDidYouRender = true
focusNameInput: HTMLInputElement | undefined = undefined
constructor(props: SortableTreeTableRowProps) {
super(props)
@ -170,19 +169,10 @@ class SortableTreeTableRow extends Component<
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)
? nextProps.property.children.map((item: any) => item.id)
: prevState.childrenExpandingIdList,
}
}
@ -208,6 +198,7 @@ class SortableTreeTableRow extends Component<
const {
property,
isExpanding,
interfaceId,
editable,
handleClickCreateChildPropertyButton,
highlightId,
@ -228,15 +219,9 @@ class SortableTreeTableRow extends Component<
{property.children
.sort((a: any, b: any) => a.priority - b.priority)
.map((item: any) => {
const childrenIsExpanding = this.state.childrenExpandingIdList.includes(
item.id
)
const childrenIsExpanding = this.state.childrenExpandingIdList.includes(item.id)
return (
<div
key={item.id}
className="SortableTreeTableRow"
data-id={item.id}
>
<div key={item.id} className="SortableTreeTableRow" data-id={item.id}>
<div
className={classNames('flex-row', {
highlight: item.id === highlightId,
@ -253,17 +238,15 @@ class SortableTreeTableRow extends Component<
this.setState(prev => ({
...prev,
childrenExpandingIdList: childrenIsExpanding
? prev.childrenExpandingIdList.filter(
id => id !== item.id
)
? prev.childrenExpandingIdList.filter(id => id !== item.id)
: [...prev.childrenExpandingIdList, item.id],
}))
}}
>
{childrenIsExpanding ? (
<GoChevronDown className="fontsize-14 color-6"/>
<GoChevronDown className="fontsize-14 color-6" />
) : (
<GoChevronRight className="fontsize-14 color-6"/>
<GoChevronRight className="fontsize-14 color-6" />
)}
</Link>
) : null}
@ -287,24 +270,17 @@ class SortableTreeTableRow extends Component<
<GoPlus className="fontsize-14 color-6" />
</Link>
) : null}
<Link
to=""
onClick={e => handleDeleteMemoryProperty(e, item)}
>
<Link to="" onClick={e => handleDeleteMemoryProperty(e, item)}>
<GoTrashcan className="fontsize-14 color-6" />
</Link>
</>
)}
</div>
<div
className={`td payload name depth-${item.depth} nowrap`}
>
<div className={`td payload name depth-${item.depth} nowrap`}>
{!editable ? (
<>
<CopyToClipboard text={item.name} type="right">
<span className="name-wrapper nowrap">
{item.name}
</span>
<span className="name-wrapper nowrap">{item.name}</span>
</CopyToClipboard>
{item.scope === 'request' && item.depth === 0 ? (
<div style={{ margin: '1px 0 0 3px' }}>
@ -321,11 +297,7 @@ class SortableTreeTableRow extends Component<
}}
value={item.name}
onChange={e =>
handleChangePropertyField(
item.id,
'name',
e.target.value
)
handleChangePropertyField(item.id, 'name', e.target.value)
}
className="form-control editable"
spellCheck={false}
@ -333,18 +305,12 @@ class SortableTreeTableRow extends Component<
/>
)}
</div>
<div
className={`td payload required type depth-${item.depth} nowrap`}
>
<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
)
handleChangePropertyField(item.id, 'required', e.target.checked)
}
color="primary"
inputProps={{
@ -364,18 +330,12 @@ class SortableTreeTableRow extends Component<
onChange={e => {
const type = e.target.value
if (isNoValueType(type)) {
handleChangeProperty(
item.id,
{
value: '',
type,
}
)
handleChangeProperty(item.id, {
value: '',
type,
})
} else {
handleChangeProperty(
item.id,
{type}
)
handleChangeProperty(item.id, { type })
}
}}
className="form-control editable"
@ -395,11 +355,7 @@ class SortableTreeTableRow extends Component<
<input
value={item.rule || ''}
onChange={e =>
handleChangePropertyField(
item.id,
'rule',
e.target.value
)
handleChangePropertyField(item.id, 'rule', e.target.value)
}
className="form-control editable"
spellCheck={false}
@ -410,19 +366,13 @@ class SortableTreeTableRow extends Component<
<div className="td payload value">
{!editable ? (
<CopyToClipboard text={item.value}>
<span className="value-container">
{getFormattedValue(item)}
</span>
<span className="value-container">{getFormattedValue(item)}</span>
</CopyToClipboard>
) : (
<SmartTextarea
value={item.value || ''}
onChange={(e: any) =>
handleChangePropertyField(
item.id,
'value',
e.target.value
)
handleChangePropertyField(item.id, 'value', e.target.value)
}
disabled={isNoValueType(item.type) && !item.value}
rows="1"
@ -441,11 +391,7 @@ class SortableTreeTableRow extends Component<
<SmartTextarea
value={item.description || ''}
onChange={(e: any) =>
handleChangePropertyField(
item.id,
'description',
e.target.value
)
handleChangePropertyField(item.id, 'description', e.target.value)
}
rows="1"
className="form-control editable"
@ -457,7 +403,14 @@ class SortableTreeTableRow extends Component<
</div>
{item.children && item.children.length ? (
<SortableTreeTableRow
{...this.props}
editable={editable}
highlightId={highlightId}
interfaceId={interfaceId}
handleClickCreateChildPropertyButton={handleClickCreateChildPropertyButton}
handleDeleteMemoryProperty={handleDeleteMemoryProperty}
handleChangeProperty={handleChangeProperty}
handleChangePropertyField={handleChangePropertyField}
handleSortProperties={handleSortProperties}
property={item}
isExpanding={childrenIsExpanding}
/>
@ -473,13 +426,29 @@ class SortableTreeTableRow extends Component<
}
class SortableTreeTable extends Component<any, any> {
render() {
const { root, editable } = this.props
const {
root,
editable,
highlightId,
interfaceId,
handleClickCreateChildPropertyButton,
handleDeleteMemoryProperty,
handleChangeProperty,
handleChangePropertyField,
handleSortProperties,
} = this.props
return (
<div className={`SortableTreeTable ${editable ? 'editable' : ''}`}>
<SortableTreeTableHeader {...this.props} />
<SortableTreeTableRow
{...this.props}
interfaceId={this.props.interfaceId}
editable={editable}
highlightId={highlightId}
handleClickCreateChildPropertyButton={handleClickCreateChildPropertyButton}
handleDeleteMemoryProperty={handleDeleteMemoryProperty}
handleChangeProperty={handleChangeProperty}
handleChangePropertyField={handleChangePropertyField}
handleSortProperties={handleSortProperties}
interfaceId={interfaceId}
property={root}
isExpanding={true}
/>
@ -488,7 +457,8 @@ class SortableTreeTable extends Component<any, any> {
}
}
class PropertyList extends Component<any, any> {
class PropertyList extends PureComponent<any, any> {
static propTypes = {
title: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
@ -497,7 +467,7 @@ class PropertyList extends Component<any, any> {
auth: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
mod: PropTypes.object.isRequired,
itf: PropTypes.object.isRequired,
interfaceId: PropTypes.number.isRequired,
editable: PropTypes.bool.isRequired,
/** optional */
bodyOption: PropTypes.string,
@ -524,16 +494,16 @@ class PropertyList extends Component<any, any> {
properties = [],
repository = {},
mod = {},
itf = {},
interfaceId,
} = this.props
if (!itf.id) {
if (!interfaceId) {
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)
const pos = rptFromStr2Num(requestParamsType)
if (scope === 'request' && editable) {
scopedProperties = scopedProperties.filter((s: any) => s.pos === pos)
}
@ -564,20 +534,15 @@ 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
}
interfaceId={interfaceId}
handleClickCreateChildPropertyButton={this.handleClickCreateChildPropertyButton}
handleDeleteMemoryProperty={this.handleDeleteMemoryProperty}
handleChangePropertyField={this.handleChangePropertyField}
handleChangeProperty={this.handleChangeProperty}
handleSortProperties={this.handleSortProperties}
handleClickCreatePropertyButton={
this.handleClickCreatePropertyButton
}
handleClickCreatePropertyButton={this.handleClickCreatePropertyButton}
/>
</div>
<div className="footer">
@ -586,7 +551,7 @@ class PropertyList extends Component<any, any> {
scope={scope}
label={label}
properties={properties}
itf={itf}
interfaceId={interfaceId}
/>
)}
</div>
@ -600,7 +565,7 @@ class PropertyList extends Component<any, any> {
scope={scope}
repository={repository}
mod={mod}
itf={itf}
interfaceId={interfaceId}
/>
</RModal>
<RModal
@ -613,7 +578,7 @@ class PropertyList extends Component<any, any> {
scope={scope}
repository={repository}
mod={mod}
itf={itf}
interfaceId={interfaceId}
parent={this.state.createChildProperty}
/>
</RModal>
@ -626,7 +591,7 @@ class PropertyList extends Component<any, any> {
title={`导入${label}属性`}
repository={repository}
mod={mod}
itf={itf}
interfaceId={interfaceId}
scope={scope}
/>
</RModal>
@ -635,13 +600,10 @@ class PropertyList extends Component<any, any> {
}
handleClickCreatePropertyButton = () => {
this.handleClickCreateChildPropertyButton()
};
// handlefocused = () => {
// this.setState({ highlightId: undefined })
// }
}
handleClickCreateChildPropertyButton = (parent: any = { id: -1 }) => {
const { handleAddMemoryProperty } = this.context
const { auth, scope, repository = {}, mod = {}, itf = {} } = this.props
const { auth, scope, repository = {}, mod = {}, interfaceId } = this.props
const childId = _.uniqueId('memory-')
const child = {
...mockProperty(),
@ -649,7 +611,7 @@ class PropertyList extends Component<any, any> {
creatorId: auth.id,
repositoryId: repository.id,
moduleId: mod.id,
interfaceId: itf.id,
interfaceId,
scope,
parentId: parent.id,
}
@ -659,43 +621,41 @@ 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 })
};
}
handleChangeProperty = (id: any, value: any) => {
const { handleChangeProperty } = this.props
const { properties } = this.props
const property = properties.find((property: any) => property.id === id)
handleChangeProperty({ ...property, ...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

@ -178,7 +178,7 @@ function RapperInstallerModal({
/>
</div>
<p className={classes.step}>1. rapper </p>
<pre>npm install rap --save-dev</pre>
<pre>npm install rap</pre>
<p className={classes.step}>2. package.json scripts </p>
<pre>
{codeTmpl({ projectId: repository.id, token: repository.token, rapperType, rapperPath })}

File diff suppressed because one or more lines are too long

@ -15,10 +15,9 @@
a, .fake-link
margin-right: 1rem
> .desc
white-space: pre-wrap
margin: 1rem 0 .5rem
color: #666
> .body
// padding: 0 2rem
.RelatedProjects
display: flex
@ -151,7 +150,12 @@
flex-grow: 1
.InterfaceList
position: sticky
top: 10px
height: calc(100vh - 10px)
.header
.newIntf
margin-bottom: 1rem
margin-bottom: 1rem
ul.body
margin: 0
@ -159,7 +163,7 @@
border: 1px solid rgba(63, 81, 181, 0.5)
border-radius: .4rem
list-style: none
max-height: 150vh
max-height: calc(100vh - 115px)
overflow-y: auto
> li
position: relative
@ -316,94 +320,94 @@
.SortableTreeTable
.SortableTreeTableHeader,
.SortableTreeTableRow
.flex-row
display: flex
&.highlight
.thtd
animation: hightlight 1.5s 1
.thtd
border: 1px solid #eceeef
flex-grow: 1
//
.flex-row
display: flex
align-items: center
flex-direction: row
margin-right: -1px
.th
@extend .thtd
padding: .75rem
font-weight: bold
.td
@extend .thtd
padding: .5rem .75rem
margin-bottom: -1px
.th, .td
&.operations
padding: .5rem
width: 1rem
&.name
width: 20rem
flex-grow: 3
&.type
width: 7rem
&.rule
width: 10rem
&.value
width: 10rem
&.desc
width: 15rem
.th
.helper
margin-left: .5rem
color: #999
&:hover
color: $brand
.td
&.operations
height: auto
line-height: 1
justify-content: flex-end
a
color: #999
margin-right: .5rem
&:last-child
margin-right: 0
&:hover
color: $brand
&.payload
&.highlight
.thtd
animation: hightlight 1.5s 1
.thtd
border: 1px solid #eceeef
flex-grow: 1
//
display: flex
align-items: center
flex-direction: row
margin-right: -1px
.th
@extend .thtd
padding: .75rem
font-weight: bold
.td
@extend .thtd
padding: .5rem .75rem
height: auto
line-height: 1.5
&.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
&:after
display: block
content: ''
position: absolute
top: 0
bottom: 0
left: 0
width: $i * 1rem
opacity: .5
border-right: 1px dashed #707070
&.payload.value
max-height: 30vh
overflow: auto
hyphens: auto
overflow-wrap: break-word
span.value-container
white-space: pre
margin: auto 0
&.payload.required
padding: 0
&.payload.desc
word-break: break-word
margin-bottom: -1px
.th, .td
&.operations
padding: .5rem
width: 1rem
&.name
width: 20rem
flex-grow: 3
&.type
width: 7rem
&.rule
width: 10rem
&.value
width: 10rem
&.desc
width: 15rem
.th
.helper
margin-left: .5rem
color: #999
&:hover
color: $brand
.td
&.operations
height: auto
line-height: 1
justify-content: flex-end
a
color: #999
margin-right: .5rem
&:last-child
margin-right: 0
&:hover
color: $brand
&.payload
padding: .5rem .75rem
height: auto
line-height: 1.5
&.payload.name
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
&:after
display: block
content: ''
position: absolute
top: 0
bottom: 0
left: 0
width: $i * 1rem
opacity: .5
border-right: 1px dashed #707070
&.payload.value
max-height: 30vh
overflow: auto
hyphens: auto
overflow-wrap: break-word
span.value-container
white-space: pre
margin: auto 0
&.payload.required
padding: 0
&.payload.desc
word-break: break-word
.SortableTreeTable.editable
.flex-row
.th, .td

@ -9,6 +9,7 @@ import InterfaceList from './InterfaceList'
import InterfaceEditor from './InterfaceEditor'
import DuplicatedInterfacesWarning from './DuplicatedInterfacesWarning'
import RapperInstallerModal from './RapperInstallerModal'
import ImportSwaggerRepositoryForm from '../repository/ImportSwaggerRepositoryForm'
import {
addRepository,
@ -40,7 +41,6 @@ import {
GoRepo,
GoPlug,
GoDatabase,
GoJersey,
GoLinkExternal,
GoPencil,
GoCode,
@ -72,6 +72,7 @@ interface States {
defaultValuesModalOpen: boolean
update: boolean
exportPostman: boolean
importSwagger: boolean
}
// 展示组件
@ -108,18 +109,32 @@ class RepositoryEditor extends Component<Props, States> {
exportPostman: false,
rapperInstallerModalOpen: false,
defaultValuesModalOpen: false,
importSwagger: false,
}
}
getChildContext() {
return _.pick(this.props, Object.keys(RepositoryEditor.childContextTypes))
}
changeDocumentTitle() {
const repository = this.props.repository.data
if (repository.name) {
document.title = `RAP2 ${repository.name}`
}
}
componentDidUpdate() {
this.changeDocumentTitle()
}
componentDidMount() {
// const id = +this.props.location.params.id
// if (!this.props.repository.data || this.props.repository.data.id !== id) {
// this.props.onFetchRepository({ id })
// }
this.changeDocumentTitle()
}
componentWillUnmount() {
document.title = `RAP2`
}
render() {
const {
location: { params },
@ -165,9 +180,7 @@ class RepositoryEditor extends Component<Props, States> {
<span className="title">
<GoRepo className="mr6 color-9" />
<Link to={`${ownerlink}`}>
{repository.organization
? repository.organization.name
: repository.owner.fullname}
{repository.organization ? repository.organization.name : repository.owner.fullname}
</Link>
<span className="slash"> / </span>
<span>{repository.name}</span>
@ -176,10 +189,7 @@ class RepositoryEditor extends Component<Props, States> {
{/* 编辑权限:拥有者或者成员 */}
{isOwned || isJoined ? (
<span
className="fake-link edit"
onClick={() => this.setState({ update: true })}
>
<span className="fake-link edit" onClick={() => this.setState({ update: true })}>
<GoPencil />
</span>
) : null}
@ -208,13 +218,22 @@ class RepositoryEditor extends Component<Props, States> {
>
<GoDatabase />
</a>
<span
className="fake-link edit"
onClick={() => this.setState({ exportPostman: true })}
>
<span className="fake-link edit" onClick={() => this.setState({ importSwagger: true })}>
<GoLinkExternal /> Swagger
</span>
<ImportSwaggerRepositoryForm
open={this.state.importSwagger}
onClose={ok => {
ok && this.handleUpdate()
this.setState({ importSwagger: false })
}}
repositoryId={repository.id}
orgId={(repository.organization || {}).id}
mode="manual"
/>
<span className="fake-link edit" onClick={() => this.setState({ exportPostman: true })}>
<GoLinkExternal />
</span>
<ExportPostmanForm
title="导出"
open={this.state.exportPostman}
@ -225,7 +244,8 @@ class RepositoryEditor extends Component<Props, States> {
className="fake-link edit"
onClick={() => this.setState({ defaultValuesModalOpen: true })}
>
<GoEllipsis />
<GoEllipsis />
</span>
<DefaultValueModal
open={this.state.defaultValuesModalOpen}
@ -250,24 +270,10 @@ class RepositoryEditor extends Component<Props, States> {
<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 >
@ -276,7 +282,7 @@ class RepositoryEditor extends Component<Props, States> {
handleUpdate = () => {
const { pathname, hash, search } = this.props.router.location
this.props.replace(pathname + search + hash)
};
}
}
// 容器组件
@ -306,7 +312,4 @@ const mapDispatchToProps = {
onSortPropertyList: sortPropertyList,
replace,
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(RepositoryEditor)
export default connect(mapStateToProps, mapDispatchToProps)(RepositoryEditor)

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["Organization.sass"],"names":[],"mappings":"AAAA;EACE;;AACA;EACE;;AACA;EACE;;AACJ;EACE;EACA;EACA;;AACA;EACE;EACA;;AACF;EACE;EACA;;AACF;EACE;;AAMF;EACE;;AAMI;EACE;EACA;;AACF;EACE;EACA;EACA;;AAEF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AACF;EACE;;AACF;EACE;;AAGJ;EACE","file":"Organization.css"}
{"version":3,"sourceRoot":"","sources":["Organization.sass","../../assets/variables.sass"],"names":[],"mappings":"AACA;EACE;;AACA;EACE;;AACA;EACE;;AACJ;EACE;EACA;EACA;;AACA;EACE;EACA;;AACF;EACE;EACA;;AACF;EACE;;;AAEN;EACE;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;EACA;EACA;;AACF;EACE;;AACA;EACE;EACA;EACA;EACA;;AACF;EACE;;AACF;EACE;;AACJ;EAEE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AACA;EACE,OCxDE;;ADyDV;EACE;EACA;EACA;EACA;;AACA;EACE;;AACF;EACE;;AAGF;EACE","file":"Organization.css"}

@ -1,3 +1,4 @@
@import "../../assets/variables.sass"
.OrganizationListWrapper
padding: 2rem
> .header
@ -5,8 +6,8 @@
.title
font-size: 2rem
> .toolbar
margin-bottom: 1rem
padding-bottom: 1rem
margin-bottom: 2rem
padding-bottom: 2rem
border-bottom: 1px solid #e1e4e8
select.OrganizationsTypeDropdown
margin-right: .5rem
@ -16,41 +17,59 @@
font-size: 1.4rem
.form-control
margin-bottom: 0
> .body
.table
> .footer
.OrganizationList
.Organization.card
margin-bottom: 1rem
// transition: box-shadow .15s ease-out
// &:hover
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.18)
.card-block
> .header
.title
margin-bottom: .5rem
font-size: 1.4rem
.toolbar
float: right
display: none
margin-left: 1rem
> .body
.desc
margin-bottom: .5rem
color: #666
.members
.avatar
margin-right: .2rem
width: 2.5rem
height: 2.5rem
border-radius: 50%
.avatar.owner
margin-right: 1rem
.popover
white-space: nowrap
.card-block:hover
> .header
.toolbar
display: inline-block
.OrganizationList
display: grid
grid-template-columns: repeat(4, 1fr)
grid-column-gap: 20px
padding: 0 15px
.Organization.card
margin-bottom: 1rem
.card-block
position: relative
.name
font-size: 1.4rem
white-space: nowrap
overflow: hidden
.desc
margin-top: 1rem
height: 6rem
overflow: hidden
color: #666
.members
color: #666
.avatar
margin-right: .2rem
width: 2.5rem
height: 2.5rem
border-radius: 50%
.avatar.owner
margin-right: 1rem
.popover
white-space: nowrap
.toolbar
// display: none
position: absolute
top: 1.2rem
right: 1.25rem
background-color: white
> a, > .fake-link
margin: 0 0 0 0.5rem
font-size: 1.4rem
color: #999
&:hover
color: $brand
.card-block.card-footer
padding-top: 0
background-color: white
border-top: none
color: #666
.ownername
float: left
.fromnow
float: right
.Organization.card:hover
.card-block
.toolbar
display: block

@ -1,11 +1,11 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { Popover } from '../utils'
import OrganizationForm from './OrganizationForm'
import { GoOrganization } from 'react-icons/go'
import { GoOrganization, GoPencil, GoSignOut, GoSignIn, GoTrashcan } from 'react-icons/go'
import { useSelector } from 'react-redux'
import { RootState, Organization } from '../../actions/types'
import { useHandleDelete, useHandleExit, useHandleJoin } from './OrganizationListParts'
import { Card } from '@material-ui/core'
function avatar(user: any) {
return `https://work.alibaba-inc.com/photo/${user.empId}.220x220.jpg`
@ -26,97 +26,73 @@ function OrganizationBlock(props: Props) {
const handleExit = useHandleExit()
const handleJoin = useHandleJoin()
return (
<section className="Organization card">
<Card className="Organization card">
<div className="card-block">
<div className="header clearfix">
<span className="title">
<GoOrganization className="mr6 color-9" />
<Link
to={`/organization/repository?organization=${organization.id}`}
>
{organization.name}
</Link>
</span>
<span className="toolbar">
{owned || joined ? ( // 拥有或已加入
<span
className="fake-link operation mr5"
onClick={() => setUpdate(true)}
>
</span>
) : null}
<OrganizationForm
organization={organization}
open={update}
onClose={() => setUpdate(false)}
/>
{owned ? ( // 拥有
<Link
to=""
onClick={e => {
e.preventDefault()
handleDelete(organization)
}}
className="operation mr5"
>
</Link>
) : null}
{!owned && joined ? ( // 不拥有,已加入
<Link
to=""
onClick={e => {
e.preventDefault()
handleExit(organization)
}}
className="operation mr5"
>
退
</Link>
) : null}
{!owned && !joined && selfHelpJoin ? ( // 不拥有,未加入
<Link
to=""
onClick={e => {
e.preventDefault()
handleJoin(organization)
}}
className="operation mr5"
>
</Link>
) : null}
</span>
<div className="name">
<GoOrganization className="mr6 color-9" />
<Link to={`/organization/repository?organization=${organization.id}`}>
{organization.name}
</Link>
</div>
<div className="desc">{organization.description}</div>
<div className="members">
<img
alt={organization.owner!.fullname}
src={avatar(organization.owner)}
className="avatar owner"
/>
<span>{organization.owner!.fullname}</span>
</div>
<div className="body">
<div className="desc">{organization.description}</div>
<div className="members">
<Popover
content={`${organization.owner!.fullname} ${
organization.owner!.id
}`}
<div className="toolbar">
{owned || joined ? ( // 拥有或已加入
<span
className="fake-link"
onClick={() => setUpdate(true)}
>
<GoPencil />
</span>
) : null}
<OrganizationForm
organization={organization}
open={update}
onClose={() => setUpdate(false)}
/>
{owned ? ( // 拥有
<span
className="fake-link"
onClick={e => {
e.preventDefault()
handleDelete(organization)
}}
>
<GoTrashcan/>
</span>
) : null}
{!owned && joined ? ( // 不拥有,已加入
<span
className="fake-link"
onClick={e => {
e.preventDefault()
handleExit(organization)
}}
>
<GoSignOut />
</span>
) : null}
{!owned && !joined && selfHelpJoin ? ( // 不拥有,未加入
<span
className="fake-link"
onClick={e => {
e.preventDefault()
handleJoin(organization)
}}
>
<img
alt={organization.owner!.fullname}
src={avatar(organization.owner)}
className="avatar owner"
/>
</Popover>
{organization.members!.map(user => (
<Popover key={user.id} content={`${user.fullname} ${user.id}`}>
<img
alt={user.fullname}
title={user.fullname}
src={avatar(user)}
className="avatar"
/>
</Popover>
))}
</div>
<GoSignIn />
</span>
) : null}
</div>
</div>
</section>
</Card>
)
}

@ -109,6 +109,11 @@ function OrganizationForm(props: Props) {
resolve(options.map(x => ({ label: `${x.fullname} ${x.empId || x.email}`, value: x.id })))
})
}
const newOwner = values.newOwner
? [{ label: values.newOwner.fullname, value: values.newOwner.id }]
: values.owner
? [{ label: values.owner.fullname, value: values.owner.id }]
: []
return (
<Form>
<div className="rmodal-body">
@ -118,9 +123,11 @@ function OrganizationForm(props: Props) {
{values.owner && (values.owner.id === auth.id)
? <UserList
isMulti={false}
value={values.newOwner ? [{ label: values.newOwner.fullname, value: values.newOwner.id }] : []}
value={newOwner}
loadOptions={loadUserOptions}
onChange={(users: any) => setFieldValue('newOwner', users[0])}
onChange={(users: any) => {
setFieldValue('newOwner', users[0])
}}
/>
: <div className="pt7 pl9">{values.owner!.fullname}</div>
}

@ -17,7 +17,7 @@ class OrganizationList extends Component<any, any> {
: <div className="fontsize-20 text-center p50"></div>
}
return (
<div className="OrganizationList">
<div className="OrganizationList row">
{organizations.map((organization: any) =>
<Organization key={organization.id} organization={organization} />
)}

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["ImportSwaggerRepositoryForm.sass"],"names":[],"mappings":"AACE;EACE;EACA;;AACF;EACE","file":"ImportSwaggerRepositoryForm.css"}

@ -0,0 +1,6 @@
.ImportSwaggerRepositoryForm
.rmodal-body
width: 500px
height: 160px
.input-url
flex: 1

@ -0,0 +1,224 @@
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { FormControlLabel, RadioGroup, Radio } from '@material-ui/core'
import { Formik, Field, Form } from 'formik'
import { TextField } from 'formik-material-ui'
import * as Yup from 'yup'
import {
Button,
Theme,
Dialog,
Slide,
DialogActions,
DialogContentText,
DialogContent,
DialogTitle,
} from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { ImportSwagger } from '../../actions/types'
import RepositoryService from '../../relatives/services/Repository'
import './ImportSwaggerRepositoryForm.css'
import { importSwaggerRepository } from '../../actions/repository'
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {},
form: {
minWidth: 500,
minHeight: 160,
},
alert: {
minWidth: 500,
minHeight: 7,
},
formItem: {
marginBottom: spacing(1),
},
ctl: {
marginTop: spacing(3),
},
section: {
display: 'inline',
},
}))
const schema = Yup.object().shape<Partial<ImportSwagger>>({
docUrl: Yup.string(),
swagger: Yup.string(),
})
const FORM_STATE_INIT: ImportSwagger = {
version: 1,
docUrl: '',
orgId: 0,
mode: 'manual',
swagger: '',
repositoryId: 0,
}
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />
})
interface Props {
open: boolean
onClose: (isOk?: boolean) => void
orgId?: number
repositoryId?: number
mode: string
}
function ImportSwaggerRepositoryForm(props: Props) {
const { open, onClose, orgId, mode, repositoryId } = props
const classes = useStyles()
const dispatch = useDispatch()
const [alertOpen, setAlertOpen] = useState({ op: false, msg: '' })
return (
<section className={classes.section}>
<Dialog
open={open}
onClose={(_event, reason) => reason !== 'backdropClick' && onClose()}
TransitionComponent={Transition}
>
<DialogTitle> Swagger </DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
}}
validationSchema={schema}
onSubmit={async (values, actions) => {
let swagger = values.swagger
if (!swagger) {
try {
swagger = await RepositoryService.getSwaggerRepository({
docUrl: values.docUrl,
})
} catch (error) {
setAlertOpen({
op: true,
msg:
'无法获取 Swagger 数据,请检查您的 Swagger 服务是否允许 CORS或者使用直接粘贴 JSON 导入',
})
actions.setSubmitting(false)
return
}
} else {
try {
swagger = JSON.parse(swagger)
} catch (error) {
setAlertOpen({
op: true,
msg: '解析 Swagger 失败,请检查 JSON 格式',
})
actions.setSubmitting(false)
return
}
}
const importSwagger: ImportSwagger = {
...values,
mode,
swagger,
orgId,
repositoryId,
}
dispatch(
importSwaggerRepository(importSwagger, (res: any) => {
if (res.isOk === 'success') {
setAlertOpen({ op: true, msg: '导入成功' })
window.location.reload()
} else {
setAlertOpen({ op: true, msg: '导入失败' })
}
onClose(true)
}),
)
}}
render={({ isSubmitting, setFieldValue, values }) => {
return (
<Form>
<div className="rmodal-body">
<div className={classes.formItem}>
<RadioGroup
name="radioListOp"
value={values.version}
onChange={e => {
setFieldValue('version', e.target.value)
}}
row={true}
>
<FormControlLabel value={1} control={<Radio />} label="Swagger 2.0" />
</RadioGroup>
</div>
<div className={classes.formItem}>
<Field
placeholder=""
name="docUrl"
label="从 Swagger URL 获取"
component={TextField}
fullWidth={true}
variant="outlined"
/>
</div>
<div className={classes.formItem}>
<Field
placeholder=""
name="swagger"
label="或者直接粘贴 Swagger JSON"
component={TextField}
fullWidth={true}
multiline={true}
rows="4"
variant="outlined"
/>
</div>
</div>
<div className={classes.ctl}>
<Button
type="submit"
variant="contained"
color={isSubmitting ? 'inherit' : 'primary'}
className="mr1"
disabled={isSubmitting}
>
{isSubmitting ? '导入中,由于批量导入数据量较大请耐心稍等...' : '提交'}
</Button>
{!isSubmitting && (
<Button onClick={() => onClose()} disabled={isSubmitting}>
</Button>
)}
</div>
</Form>
)
}}
/>
</div>
</DialogContent>
</Dialog>
<Dialog
open={alertOpen.op}
onClose={() => setAlertOpen({ op: false, msg: '' })}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title"></DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description" className={classes.alert}>
{alertOpen.msg}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setAlertOpen({ op: false, msg: '' })} color="primary">
</Button>
</DialogActions>
</Dialog>
</section>
)
}
export default ImportSwaggerRepositoryForm

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["Repository.sass","../../assets/variables.sass"],"names":[],"mappings":"AAEA;EACE;;AACA;EACE;;AACA;EACE;;AACJ;EACE;EACA;EACA;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;;AACF;EACE;;AACJ;EACE;;AAGJ;EACE;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;EACA;EACA;;AACF;EACE;EACA;;AACA;EACE;EACA;EACA;;AACJ;EAEE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AACA;EACE,OCzDE;;AD0DV;EACE;EACA;EACA;EACA;;AACA;EACE;;AACF;EACE;;AAGF;EACE","file":"Repository.css"}
{"version":3,"sourceRoot":"","sources":["Repository.sass","../../assets/variables.sass"],"names":[],"mappings":"AAEA;EACE;;AACA;EACE;;AACA;EACE;;AACJ;EACE;EACA;EACA;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;;AACF;EACE;;AACJ;EACE;;;AAEJ;EACE;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;EACA;EACA;;AACF;EACE;EACA;;AACA;EACE;EACA;EACA;;AACJ;EAEE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AACA;EACE,OCxDE;;ADyDV;EACE;EACA;EACA;EACA;;AACA;EACE;;AACF;EACE;;AAGF;EACE","file":"Repository.css"}

@ -7,8 +7,8 @@
.title
font-size: 2rem
> .toolbar
margin-bottom: 1rem
padding-bottom: .5rem
margin-bottom: 2rem
padding-bottom: 2rem
border-bottom: 1px solid #e1e4e8
select.RepositoriesTypeDropdown
margin-right: .5rem
@ -21,7 +21,6 @@
margin-bottom: .5rem
> .body
margin-bottom: 2rem
> .footer
.RepositoryList
display: grid

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { YUP_MSG } from '../../family/UIConst'
import { Formik, Field, Form } from 'formik'
@ -9,14 +9,15 @@ import { makeStyles } from '@material-ui/styles'
import { TransitionProps } from '@material-ui/core/transitions/transition'
import { RepositoryFormData, RootState, Repository } from '../../actions/types'
import UserList from '../common/UserList'
import Select from '../common/Select'
import AccountService from '../../relatives/services/Account'
import * as _ from 'lodash'
import { updateRepository, addRepository } from '../../actions/repository'
import { fetchOwnedOrganizationList, fetchJoinedOrganizationList } from '../../actions/organization'
import { refresh } from '../../actions/common'
const useStyles = makeStyles(({ spacing }: Theme) => ({
root: {
},
root: {},
appBar: {
position: 'relative',
},
@ -43,8 +44,10 @@ const useStyles = makeStyles(({ spacing }: Theme) => ({
},
}))
const schema = Yup.object().shape<Partial<RepositoryFormData>>({
name: Yup.string().required(YUP_MSG.REQUIRED).max(20, YUP_MSG.MAX_LENGTH(20)),
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)),
})
@ -53,6 +56,7 @@ const FORM_STATE_INIT: RepositoryFormData = {
name: '',
description: '',
members: [],
organizationId: undefined,
collaborators: [],
collaboratorIdstring: '',
}
@ -65,76 +69,103 @@ interface Props {
title?: string
open: boolean
onClose: (isOk?: boolean) => void
repository?: Repository,
repository?: Repository
organizationId?: number
}
function RepositoryForm(props: Props) {
const { open, onClose, title, organizationId } = props
const repository = props.repository as RepositoryFormData
let repository = props.repository as RepositoryFormData
if (repository) {
repository.collaboratorIdstring = repository.collaborators!.map(x => { return x.id }).join(',')
}
const auth = useSelector((state: RootState) => state.auth)
const organizations = useSelector((state: RootState) => {
return _.uniqBy([...state.ownedOrganizations.data, ...state.joinedOrganizations.data], 'id')
}).map(org => ({
label: org.name,
value: org.id,
}))
const classes = useStyles()
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchJoinedOrganizationList())
dispatch(fetchOwnedOrganizationList())
}, [])
if (repository) {
repository = { ...FORM_STATE_INIT, ...repository }
repository.collaboratorIdstring = repository
.collaborators!.map(x => {
return x.id
})
.join(',')
} else {
repository = { ...FORM_STATE_INIT }
if (organizationId !== undefined) {
repository.organizationId = organizationId
}
}
return (
<Dialog
open={open}
onClose={(_event, reason) => (reason !== 'backdropClick' && onClose())}
onClose={(_event, reason) => reason !== 'backdropClick' && onClose()}
TransitionComponent={Transition}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent dividers={true}>
<div className={classes.form}>
<Formik
initialValues={{
...FORM_STATE_INIT,
...(repository || {}),
}}
initialValues={repository}
validationSchema={schema}
onSubmit={(values) => {
onSubmit={values => {
const addOrUpdateRepository = values.id ? updateRepository : addRepository
const repository: RepositoryFormData = {
...values,
memberIds: (values.members || []).map(
(user: any) => user.id
),
collaboratorIds: (
values.collaboratorIdstring || ''
).split(','),
}
if (organizationId !== undefined) {
repository.organizationId = organizationId
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)
}))
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) => {
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 })))
resolve(
options.map(x => ({
label: `${x.fullname} ${x.empId || x.email}`,
value: x.id,
})),
)
})
}
const selectOrganization = organizations.find(
(x: any) => x.value === values.organizationId,
)
return (
<Form>
<div className="rmodal-body">
{values.id > 0 && (
<div className={classes.formItem}>
<div className={classes.formTitle}>
</div>
<div className={classes.formTitle}></div>
{values.owner &&
values.owner.id === auth.id ? (
{values.owner && values.owner.id === auth.id ? (
<UserList
isMulti={false}
loadOptions={loadUserOptions}
@ -142,31 +173,21 @@ function RepositoryForm(props: Props) {
values.owner
? [
{
label:
values.owner.fullname,
label: values.owner.fullname,
value: values.owner.id,
},
]
: []
}
onChange={(users: any) =>
setFieldValue('newOwner', users[0])
}
onChange={(users: any) => setFieldValue('newOwner', users[0])}
/>
) : (
<div className="pt7 pl9">
{values.owner!.fullname}
</div>
<div className="pt7 pl9">{values.owner!.fullname}</div>
)}
</div>
)}
<div className={classes.formItem}>
<Field
name="name"
label="仓库名称"
component={TextField}
fullWidth={true}
/>
<Field name="name" label="仓库名称" component={TextField} fullWidth={true} />
</div>
<div className={classes.formItem}>
<Field
@ -178,9 +199,7 @@ function RepositoryForm(props: Props) {
/>
</div>
<div className={classes.formItem}>
<div className={classes.formTitle}>
</div>
<div className={classes.formTitle}></div>
<UserList
isMulti={true}
loadOptions={loadUserOptions}
@ -188,9 +207,21 @@ function RepositoryForm(props: Props) {
label: x.fullname,
value: x.id,
}))}
onChange={selected =>
setFieldValue('members', selected)
}
onChange={selected => setFieldValue('members', selected)}
/>
</div>
<div className={classes.formItem}>
<div className={classes.formTitle}></div>
<Select
isMulti={false}
isClearable={true}
options={organizations}
value={selectOrganization}
onChange={val => {
if (!Array.isArray(val)) {
setFieldValue('organizationId', val && val.value)
}
}}
/>
</div>
<div className={classes.formItem}>
@ -212,10 +243,7 @@ function RepositoryForm(props: Props) {
>
</Button>
<Button
onClick={() => onClose()}
disabled={isSubmitting}
>
<Button onClick={() => onClose()} disabled={isSubmitting}>
</Button>
</div>

@ -11,12 +11,13 @@ import { GoArrowRight } from 'react-icons/go'
import { Organization, RootState } from 'actions/types'
import { useDispatch, useSelector } from 'react-redux'
import { Select, MenuItem, TextField, Button } from '@material-ui/core'
import OrganizationForm from 'components/organization/OrganizationForm'
export const mapDispatchToProps = ({
export const mapDispatchToProps = {
onAddRepository: addRepository,
onUpdateRepository: updateRepository,
onDeleteRepository: deleteRepository,
})
}
interface CreateButtonProps {
organization?: Organization
@ -27,6 +28,7 @@ export function CreateButton(props: CreateButtonProps) {
const { organization, callback } = props
const [creating, setCreating] = useState(false)
const [importing, setImporting] = useState(false)
const [updateOrganization, setUpdateOrganization] = useState(false)
const dispatch = useDispatch()
const router = useSelector((state: RootState) => state.router)
const handleUpdate = () => {
@ -37,9 +39,27 @@ export function CreateButton(props: CreateButtonProps) {
dispatch(replace(uri.href()))
}
}
return (
<span className="float-right ml10">
{/* DONE 2.1 √我加入的仓库、X所有仓库 是否应该有 新建仓库 */}
{organization && (
<Button
style={{ marginRight: 8 }}
className="RepositoryCreateButton"
variant="contained"
color="primary"
onClick={() => setUpdateOrganization(true)}
>
</Button>
)}
<OrganizationForm
organization={organization}
open={updateOrganization}
onClose={() => setUpdateOrganization(false)}
/>
<Button
className="RepositoryCreateButton"
variant="contained"
@ -49,9 +69,16 @@ export function CreateButton(props: CreateButtonProps) {
</Button>
<RepositoryForm
title="新建仓库"
open={creating}
onClose={() => setCreating(false)}
organizationId={organization ? organization.id : undefined}
/>
{organization && (
<Button
style={{marginLeft: 8}}
style={{ marginLeft: 8 }}
className="RepositoryCreateButton"
variant="contained"
color="primary"
@ -61,13 +88,6 @@ export function CreateButton(props: CreateButtonProps) {
</Button>
)}
<RepositoryForm
title="新建仓库"
open={creating}
onClose={() => setCreating(false)}
organizationId={organization ? organization.id : undefined}
/>
{organization && (
<RModal
when={importing && !!organization}
@ -89,11 +109,7 @@ export function RepositoriesTypeDropdown(props: { url: string }) {
dispatch(push(url))
}
return (
<Select
className="mr8"
value={url}
onChange={e => handlePush(e.target.value as string)}
>
<Select className="mr8" value={url} onChange={e => handlePush(e.target.value as string)}>
<MenuItem value="/repository/joined"></MenuItem>
<MenuItem value="/repository/all"></MenuItem>
</Select>
@ -126,17 +142,23 @@ export function SearchGroup(props: { name: string }) {
)
}
export const RepositoryListWithSpin = ({ name, repositories }: any) => (
repositories.fetching
? <Spin />
: <RepositoryList name={name} repositories={repositories.data} editor="/repository/editor" />
)
export const OrganizationRepositoryListWithSpin = ({ name, repositories }: any) => (
repositories.fetching
? <Spin />
: <RepositoryList name={name} repositories={repositories.data} editor="/organization/repository/editor" />
)
export const RepositoryListWithSpin = ({ name, repositories }: any) =>
repositories.fetching ? (
<Spin />
) : (
<RepositoryList name={name} repositories={repositories.data} editor="/repository/editor" />
)
export const OrganizationRepositoryListWithSpin = ({ name, repositories }: any) =>
repositories.fetching ? (
<Spin />
) : (
<RepositoryList
name={name}
repositories={repositories.data}
editor="/organization/repository/editor"
/>
)
export class PaginationWithLocation extends Component<any, any> {
static contextTypes = {

@ -25,4 +25,3 @@
> .header > .title
font-size: 2rem
margin-bottom: 1rem
.footer

@ -50,4 +50,4 @@
.card-title
font-size: 1.2rem
margin-bottom: 1rem
ul.fields

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["Pagination.sass","../../assets/variables.sass"],"names":[],"mappings":"AAGE;EACE;EACA;;AACF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;EACA;EACA;;AACA;EACE;EACA,kBCfI;;ADkBR;EACE;EACA;EACA;;AAGF;EACE;EACA,kBC3BM","file":"Pagination.css"}
{"version":3,"sourceRoot":"","sources":["Pagination.sass","../../assets/variables.sass"],"names":[],"mappings":"AAEA;EACE;EACA;;AACA;EACE;EACA;EACA;;AACF;EACE;EACA;EACA;EACA;;AACA;EACE;;AACA;EACE;EACA;EACA;;AACA;EACE;EACA,kBCjBI;;ADoBR;EACE;EACA;EACA;;AAGF;EACE;EACA,kBC7BM","file":"Pagination.css"}

@ -1,13 +1,15 @@
@import "../../assets/variables.sass"
.Pagination
display: flex
width: 100%
.summary
float: left
align-self: center
padding: 0.5rem 0
flex: 1
.page-list
float: right
display: flex
margin-bottom: 0
margin: 0
padding-left: 0
list-style: none
.page-item

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["PropertyEditor.sass"],"names":[],"mappings":"AAQE;EACE;;AACF;EACE;EACA;;AACF;EACE;;AACF;EACE;;AACF;EACE;;AAEF;EACE;;AACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;;AACF;EACE","file":"PropertyEditor.css"}
{"version":3,"sourceRoot":"","sources":["PropertyEditor.sass"],"names":[],"mappings":"AAGE;EACE;;AACF;EACE;EACA;;AACF;EACE;;AACF;EACE;;AACF;EACE;;AAEF;EACE;;AACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;EACA;EACA;EACA;EACA;EACA;EACA;;AACF;EACE","file":"PropertyEditor.css"}

@ -1,11 +1,6 @@
@import "../../assets/variables.sass"
.PropertyEditor
.RuleEditor
// display: flex
// flex-direction: row
// align-items: center
.type
flex-basis: 10rem
.rule-wrapper

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["RadioList.sass"],"names":[],"mappings":"AAAA;EACE;;AAGA;EACE","file":"RadioList.css"}
{"version":3,"sourceRoot":"","sources":["RadioList.sass"],"names":[],"mappings":"AAAA;EACE;;AACA;EACE","file":"RadioList.css"}

@ -1,6 +1,4 @@
.ctl-radio-list
line-height: 32px
.label
.input
margin: 0 6px 0 3px

@ -0,0 +1,75 @@
import React, { useState, useCallback, Fragment } from 'react'
import ConfirmationDialog from 'components/common/ConfirmDialog'
interface Options {
title?: string
content: React.ReactNode
type: 'alert' | 'confirm'
}
interface CallOptions {
title?: string
content: React.ReactNode
}
interface GlobalContext {
alert: (options: CallOptions) => Promise<void>
confirm: (options: CallOptions) => Promise<void>
}
const defaultOptions: Options = {
title: '确认',
content: '',
type: 'alert',
}
export const GlobalContext = React.createContext<GlobalContext>({
alert: () => Promise.resolve(),
confirm: () => Promise.resolve()
})
export const GlobalProvider = ({ children }: { children: React.ReactNode }) => {
const [options, setOptions] = useState<Options>({ ...defaultOptions })
const [resolveReject, setResolveReject] = useState<Array<() => void>>([])
const [resolve, reject] = resolveReject
const confirm = useCallback((options: CallOptions) => {
return new Promise<void>((resolve, reject) => {
setOptions({ ...options, type: 'confirm' })
setResolveReject([resolve, reject])
})
}, [])
const alert = useCallback((options: CallOptions) => {
return new Promise<void>((resolve, reject) => {
setOptions({ ...options, type: 'alert' })
setResolveReject([resolve, reject])
})
}, [])
const handleClose = useCallback(() => {
setResolveReject([])
}, [])
const handleCancel = useCallback(() => {
reject()
handleClose()
}, [reject, handleClose])
const handleConfirm = useCallback(() => {
resolve()
handleClose()
}, [resolve, handleClose])
return (
<Fragment>
<GlobalContext.Provider value={{ confirm, alert }}>{children}</GlobalContext.Provider>
<ConfirmationDialog
open={resolveReject.length === 2}
{...options}
onCancel={handleCancel}
onConfirm={handleConfirm}
/>
</Fragment>
)
}

@ -6,6 +6,7 @@ import { History } from 'history'
import { ConnectedRouter } from 'connected-react-router'
import { MuiThemeProvider } from '@material-ui/core/styles/'
import { SnackbarProvider } from 'notistack'
import { GlobalProvider } from './GlobalProvider'
import { withStyles } from '@material-ui/core'
import GlobalStyles from '../components/common/GlobalStyles'
import MuiTheme from '../components/common/MuiTheme'
@ -28,11 +29,13 @@ const start = (
return (
<MuiThemeProvider theme={MuiTheme}>
<SnackbarProvider maxSnack={3}>
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes/>
</ConnectedRouter>
</Provider>
<GlobalProvider>
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes />
</ConnectedRouter>
</Provider>
</GlobalProvider>
</SnackbarProvider>
</MuiThemeProvider>
)

@ -0,0 +1,7 @@
import { useContext } from 'react'
import { GlobalContext } from 'family/GlobalProvider'
export const useAlert = () => {
const { confirm } = useContext(GlobalContext)
return confirm
}

@ -0,0 +1,7 @@
import { useContext } from 'react'
import { GlobalContext } from 'family/GlobalProvider'
export const useConfirm = () => {
const { confirm } = useContext(GlobalContext)
return confirm
}

@ -15,6 +15,10 @@ import Spin from './components/utils/Spin'
import * as AccountAction from './actions/account'
import AccountService from './relatives/services/Account'
if (process.env.NODE_ENV !== 'production') {
const whyDidYouRender = require('@welldone-software/why-did-you-render/dist/no-classes-transpile/umd/whyDidYouRender.min.js')
whyDidYouRender(React)
}
// 渲染整站开屏动画
function* renderOpeningScreenAdvertising() {
yield new Promise((resolve) => {

@ -5,12 +5,11 @@ import * as InterfaceEffects from './effects/interface'
import * as PropertyEffects from './effects/property'
export default {
reducers: {
},
reducers: {},
sagas: {
MODULE_ADD: ModuleEffects.addModule,
MODULE_UPDATE: ModuleEffects.updateModule,
MODULE_MOVE: ModuleEffects.moveModule,
MODULE_DELETE: ModuleEffects.deleteModule,
INTERFACE_ADD: InterfaceEffects.addInterface,

@ -426,6 +426,8 @@ export default {
RepositoryEffects.fetchRepositoryList,
[RepositoryAction.importRepository(undefined, undefined).type]:
RepositoryEffects.importRepository,
[RepositoryAction.importSwaggerRepository(undefined, undefined).type]:
RepositoryEffects.importSwaggerRepository,
[RepositoryAction.fetchOwnedRepositoryList().type]:
RepositoryEffects.fetchOwnedRepositoryList,
[RepositoryAction.fetchJoinedRepositoryList().type]:

@ -1,7 +1,9 @@
import {
call,
put
put,
select
} from 'redux-saga/effects'
import { RootState } from 'actions/types'
import * as InterfaceAction from '../../actions/interface'
import EditorService from '../services/Editor'
import * as RepositoryAction from '../../actions/repository'
@ -30,15 +32,18 @@ export function* updateInterface(action: any) {
}
export function* moveInterface(action: any) {
try {
const {
repoId,
...params
} = action.params
const params = action.params
const currRepositoryId = yield select(
(state: RootState) => state.repository && state.repository.data && state.repository.data.id,
)
yield call(EditorService.moveInterface, params)
yield put(RepositoryAction.fetchRepository({
id: repoId,
repository: undefined,
}))
yield put(InterfaceAction.moveInterfaceSucceeded())
yield put(
RepositoryAction.fetchRepository({
id: currRepositoryId,
repository: undefined,
}),
)
action.onResolved && action.onResolved()
} catch (e) {
console.error(e.message)

@ -1,8 +1,11 @@
import {
call,
put
put,
select
} from 'redux-saga/effects'
import { RootState } from 'actions/types'
import * as ModuleAction from '../../actions/module'
import * as InterfaceAction from '../../actions/interface'
import * as RepositoryAction from '../../actions/repository'
import EditorService from '../services/Editor'
@ -45,6 +48,27 @@ export function* updateModule(action: any) {
if (action.onRejected) { action.onRejected() }
}
}
export function* moveModule(action: any) {
try {
const params = action.params
const currRepositoryId = yield select(
(state: RootState) => state.repository && state.repository.data && state.repository.data.id,
)
yield call(EditorService.moveModule, params)
yield put(InterfaceAction.moveInterfaceSucceeded())
yield put(
RepositoryAction.fetchRepository({
id: currRepositoryId,
repository: undefined,
}),
)
action.onResolved && action.onResolved()
} catch (e) {
console.error(e.message)
yield put(InterfaceAction.moveInterfaceFailed(e.message))
action.onRejected && action.onRejected()
}
}
export function* deleteModule(action: any) {
try {
const count = yield call(EditorService.deleteModule, action.id)

@ -70,6 +70,19 @@ export function* importRepository(action: any) {
yield put(RepositoryAction.importRepositoryFailed(e.message))
}
}
export function* importSwaggerRepository(action: any) {
try {
const res = yield call(RepositoryService.importSwaggerRepository, action.data)
if (res.isOk) {
yield put(RepositoryAction.importSwaggerRepositorySucceeded())
if (action.onResolved) { action.onResolved(res) }
} else {
throw new Error(res.message)
}
} catch (e) {
yield put(RepositoryAction.importSwaggerRepositoryFailed(e.message))
}
}
export function* fetchRepository(action: any) {
try {

@ -2,13 +2,18 @@ import { CREDENTIALS, serve } from './constant'
export default {
get({ project, module, page, property }: any) {
return fetch(`${serve}/workspace/get?project=${project}&module=${module}&page=${page}&action={action}&property=${property}`, { ...CREDENTIALS })
return fetch(
`${serve}/workspace/get?project=${project}&module=${module}&page=${page}&action={action}&property=${property}`,
{ ...CREDENTIALS },
)
.then(res => res.json())
.then(json => json.data)
},
// 模块 Module
fetchModuleList({ repositoryId = '', name = '' }: any = {}) {
return fetch(`${serve}/module/list?repositoryId=${repositoryId}&name=${name}`, { ...CREDENTIALS })
return fetch(`${serve}/module/list?repositoryId=${repositoryId}&name=${name}`, {
...CREDENTIALS,
})
.then(res => res.json())
.then(json => json.data)
},
@ -37,6 +42,16 @@ export default {
.then(res => res.json())
.then(json => json.data)
},
moveModule(params: any) {
return fetch(`${serve}/module/move`, {
...CREDENTIALS,
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(json => json.data)
},
deleteModule(id: any) {
return fetch(`${serve}/module/remove?id=${id}`, { ...CREDENTIALS })
.then(res => res.json())
@ -54,7 +69,9 @@ export default {
},
// 页面 Page
fetchPageList({ module, cursor = 1, limit = 10 }: any) {
return fetch(`${serve}/page/list?module=${module}&cursor=${cursor}&limit=${limit}`, { ...CREDENTIALS })
return fetch(`${serve}/page/list?module=${module}&cursor=${cursor}&limit=${limit}`, {
...CREDENTIALS,
})
.then(res => res.json())
.then(json => json.data)
},
@ -90,7 +107,10 @@ export default {
},
// 接口 Interface
fetchInterfaceList({ repositoryId = '', moduleId = '', name = '' }: any = {}) {
return fetch(`${serve}/interface/list?repositoryId=${repositoryId}&moduleId=${moduleId}&name=${name}`, { ...CREDENTIALS })
return fetch(
`${serve}/interface/list?repositoryId=${repositoryId}&moduleId=${moduleId}&name=${name}`,
{ ...CREDENTIALS },
)
.then(res => res.json())
.then(json => json.data)
},
@ -172,7 +192,10 @@ export default {
},
// 属性 Property
fetchPropertyList({ repositoryId = '', moduleId = '', interfaceId = '', name = '' }: any = {}) {
return fetch(`${serve}/property/list?repositoryId=${repositoryId}&moduleId=${moduleId}&interfaceId=${interfaceId}&name=${name}`, { ...CREDENTIALS })
return fetch(
`${serve}/property/list?repositoryId=${repositoryId}&moduleId=${moduleId}&interfaceId=${interfaceId}&name=${name}`,
{ ...CREDENTIALS },
)
.then(res => res.json())
.then(json => json.data)
},

@ -11,7 +11,7 @@ export default {
fetchRepositoryList({ user = '', organization = '', name = '', cursor = 1, limit = 100 }: any = {}) {
return fetch(`${serve}/repository/list?user=${user}&organization=${organization}&name=${name}&cursor=${cursor}&limit=${limit}`, { ...CREDENTIALS })
.then(res => res.json())
// .then(json => json.data)
// .then(json => json.data)
},
fetchOwnedRepositoryList({ user = '', name = '' }: any = {}) {
return fetch(`${serve}/repository/owned?user=${user}&name=${name}`, { ...CREDENTIALS })
@ -62,6 +62,22 @@ export default {
})
.then(res => res.json())
},
importSwaggerRepository(data: any) {
return fetch(`${serve}/repository/importswagger`, {
...CREDENTIALS,
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
},
getSwaggerRepository(data: any) {
return fetch(`${data.docUrl}`, {
mode: 'cors',
method: 'GET',
})
.then(res => res.json())
},
deleteRepository(id: any) {
return fetch(`${serve}/repository/remove?id=${id}`, { ...CREDENTIALS })
.then(res => res.json())

@ -8,7 +8,7 @@ import { RootState } from 'actions/types'
const interfaceSelector = (state: RootState) => {
const router = getRouter(state)
const itfId = +((router.location as any).query || (router.location as any).params).itf
const itfId = +((router.location as any).params || (router.location as any).query).itf
if (itfId > 0) {
for (const mod of state.repository.data.modules) {
for (const itf of mod.interfaces) {

@ -24,7 +24,7 @@
"strict": true,
"declaration": false,
"emitDecoratorMetadata": true,
"noUnusedLocals": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"moduleResolution": "node",
"noEmitOnError": true,

@ -89,17 +89,7 @@
"ignore-bound-class-methods"
],
"trailing-comma": [
true,
{
"singleline": "never",
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "never",
"typeLiterals": "ignore"
},
"esSpecCompliant": true
}
true
],
"triple-equals": [
true,

Loading…
Cancel
Save