Project initial

This commit is contained in:
chengpu
2022-12-03 00:54:53 +08:00
commit 5092a5e717
502 changed files with 50820 additions and 0 deletions

View File

@@ -0,0 +1,346 @@
import BodyClassName from 'react-body-classname'
import get from 'lodash.get'
import React, {lazy, Suspense, useContext, useEffect, useRef, useState,} from 'react'
import {toast} from 'react-toastify'
import useHash from './utils/Hash'
import useWindowsUtils from './utils/WindowsUtils'
import {AppContext} from './reducer/App'
import {Button, Form, RadioGroup} from './common/form'
import {
Control,
FieldError,
FieldInput,
FieldRadio,
List,
Loading,
PanelMore,
QuickSearch,
Tabs,
Warnings,
} from './common/builder'
import {Footer, Layout} from './common/layout'
import {InitializrContext} from './reducer/Initializr'
import {getConfig, getInfo, getProject, getQueryString} from './utils/ApiUtils'
const Explore = lazy(() => import('./common/explore/Explore.js'))
const Share = lazy(() => import('./common/share/Share.js'))
const Fetch = lazy(() => import('./common/fetch/Fetch.js'))
const HotKeys = lazy(() => import('./common/builder/HotKeys.js'))
export default function Application() {
const {
complete,
config,
more,
tab,
dispatch,
theme,
fetch: fetchOpen,
share: shareOpen,
explore: exploreOpen,
dependencies,
} = useContext(AppContext)
const { values, share, dispatch: dispatchInitializr, errors } = useContext(InitializrContext)
const [positionShare, setPositionShare] = useState({ x: 0, y: 0 })
const [blob, setBlob] = useState(null)
const inputMore = useRef(null)
const inputQuickSearch = useRef(null)
const windowsUtils = useWindowsUtils()
useHash()
useEffect(() => {
if (windowsUtils.origin) {
const url = `${windowsUtils.origin}/metadata/client`
getInfo(url).then(jsonConfig => {
const response = getConfig(jsonConfig)
dispatchInitializr({ type: 'COMPLETE', payload: { ...response } })
dispatch({ type: 'COMPLETE', payload: response })
})
}
}, [dispatch, dispatchInitializr, windowsUtils.origin])
const onSubmit = async () => {
dispatch({ type: 'FETCH_UPDATE', payload: { open: true } })
}
const onExplore = async () => {
const url = `${windowsUtils.origin}/starter.zip`
dispatch({ type: 'EXPLORE_UPDATE', payload: { open: true } })
const project = await getProject(
url,
values,
get(dependencies, 'list')
).catch(() => {
toast.error(`Could not connect to server. Please check your network.`)
})
setBlob(project)
}
const update = args => {
dispatchInitializr({ type: 'UPDATE', payload: args })
}
const onRunInSandbox = () => {
const origin = `${windowsUtils.origin}`
const config = get(dependencies, 'list')
const params = getQueryString(values, config, ["cloudshell"])
const gitUrl = `${origin}/${params}/${values.meta.artifact}.git`
const handsOnUrl = `${origin}/handson?${params}`
window.open(handsOnUrl)
}
const onExport = () => {
const origin = `${windowsUtils.origin}`
const config = get(dependencies, 'list')
const params = getQueryString(values, config)
const codeupUrl = `${origin}/codeup?${params}`
window.open(codeupUrl)
}
let shareSrc = get(values, 'share')
if(shareSrc){
shareSrc = `${shareSrc}/`
} else {
shareSrc = ''
}
return (
<Layout>
<BodyClassName className={theme} />
<Suspense fallback=''>
<HotKeys onSubmit={onSubmit} onExplore={onExplore} />
</Suspense>
<Form onSubmit={onSubmit}>
{!complete ? (
<Loading />
) : (
<>
<Warnings />
<Control text='项目构建方式'>
<RadioGroup
name='project'
selected={get(values, 'project')}
options={get(config, 'lists.project')}
onChange={value => {
update({ project: value })
}}
/>
</Control>
<Control text='开发语言'>
<RadioGroup
name='language'
selected={get(values, 'language')}
options={get(config, 'lists.language')}
onChange={value => {
update({ language: value })
}}
/>
</Control>
<Control text='Spring Boot版本'>
<RadioGroup
name='boot'
selected={get(values, 'boot')}
error={get(errors, 'boot.value', '')}
options={get(config, 'lists.boot')}
onChange={value => {
dispatchInitializr({
type: 'UPDATE',
payload: { boot: value },
config: get(dependencies, 'list'),
})
dispatch({
type: 'UPDATE_DEPENDENCIES',
payload: { boot: value },
})
}}
/>
{get(errors, 'boot') && (
<FieldError>
Spring Boot {get(errors, 'boot.value')} is not supported.
Please select a valid version
</FieldError>
)}
</Control>
<Control text='项目基本信息' variant='md'>
<FieldInput
id='input-group'
value={get(values, 'meta.group')}
text='Group'
onChange={event => {
update({ meta: { group: event.target.value } })
}}
/>
<FieldInput
id='input-artifact'
value={get(values, 'meta.artifact')}
text='Artifact'
onChange={event => {
update({ meta: { artifact: event.target.value } })
}}
/>
<PanelMore fieldFocusOnOpen={inputMore}>
<FieldInput
id='input-name'
value={get(values, 'meta.name')}
text='项目名称'
disabled={!more}
inputRef={inputMore}
onChange={event => {
update({ meta: { name: event.target.value } })
}}
/>
<FieldInput
id='input-description'
value={get(values, 'meta.description')}
text='项目描述'
disabled={!more}
onChange={event => {
update({ meta: { description: event.target.value } })
}}
/>
<FieldInput
id='input-packageName'
value={get(values, 'meta.packageName')}
text='基础包'
disabled={!more}
onChange={event => {
update({ meta: { packageName: event.target.value } })
}}
/>
<FieldRadio
id='input-packaging'
value={get(values, 'meta.packaging')}
text='打包方式'
disabled={!more}
options={get(config, 'lists.meta.packaging')}
onChange={value => {
update({ meta: { packaging: value } })
}}
/>
<FieldRadio
id='input-java'
value={get(values, 'meta.java')}
text='Java版本'
disabled={!more}
options={get(config, 'lists.meta.java')}
onChange={value => {
update({ meta: { java: value } })
}}
/>
</PanelMore>
</Control>
<Control text='应用架构'>
<RadioGroup
name='architecture'
selected={get(values, 'architecture')}
options={get(config, 'lists.architecture')}
onChange={value => {
update({ architecture: value })
get(values, 'dependencies', []).forEach(item => {
dispatchInitializr({
type: 'REMOVE_DEPENDENCY',
payload: { id:item },
})
})
get(config, 'lists.architecture', [])
.filter(item => item.key === value)
.forEach(arch => {
get(arch, 'dependencies', []).forEach(dep => {
dispatchInitializr({
type: 'ADD_DEPENDENCY',
payload: { id:dep },
})
})
})
}}
/>
</Control>
<Control text='组件依赖' variant={tab === 'quicksearch' ? 'xl' : 'xxl'}>
<Tabs
changeTab={newTab => {
if (
newTab === 'quicksearch' &&
get(inputQuickSearch, 'current')
) {
get(inputQuickSearch, 'current').focus()
}
}}
/>
{tab === 'quicksearch' && (
<QuickSearch submit={onSubmit} input={inputQuickSearch} />
)}
{tab === 'list' && <List />}
</Control>
<Footer>
<Button
id='generate-project'
variant='primary'
onClick={onSubmit}
hotkey={`${windowsUtils.symb} + ⏎`}
>
获取代码
</Button>
<Button
id='explore-project'
onClick={onExplore}
hotkey='Ctrl + Space'
>
浏览代码
</Button>
</Footer>
</>
)}
</Form>
<Suspense fallback=''>
<Share
open={shareOpen || false}
shareUrl={share}
shareSrc={shareSrc}
position={positionShare}
onClose={() => {
dispatch({
type: 'SHARE_UPDATE',
payload: { open: false },
})
}}
/>
<Explore
projectName={`${get(values, 'meta.artifact')}.zip`}
blob={blob}
open={exploreOpen || false}
onClose={() => {
dispatch({
type: 'EXPLORE_UPDATE',
payload: { open: false },
})
setBlob(null)
}}
/>
<Fetch
open={fetchOpen || false}
onClose={() => {
dispatch({
type: 'FETCH_UPDATE',
payload: { open: false },
})
}}
/>
</Suspense>
</Layout>
)
}

View File

@@ -0,0 +1,43 @@
import PropTypes from 'prop-types'
import React from 'react'
const Control = ({ text, children, variant }) => {
if (variant === 'xl' || variant === 'xxl') {
return (
<div className='colset'>
<div className='left'>
<div className='sticky-label'>{text}</div>
</div>
<div
className={`dependencies-box ${variant === 'xl' ? 'list' : 'large'}`}
>
{children}
</div>
</div>
)
}
return (
<div className='colset'>
<div className='left'>
<>{text}</>
</div>
<div className={`right ${variant === 'md' ? 'right-md' : ''}`}>
{children}
</div>
</div>
)
}
Control.defaultProps = {
children: null,
variant: '',
}
Control.propTypes = {
children: PropTypes.node,
variant: PropTypes.string,
text: PropTypes.string.isRequired,
}
export default Control

View File

@@ -0,0 +1,18 @@
import PropTypes from 'prop-types'
import React from 'react'
function FieldError({ children }) {
return (
<div className='control-error'>
<p className='title'>
<strong>{children}</strong>
</p>
</div>
)
}
FieldError.propTypes = {
children: PropTypes.string.isRequired,
}
export default FieldError

View File

@@ -0,0 +1,36 @@
import PropTypes from 'prop-types'
import React from 'react'
const FieldInput = ({ id, text, value, onChange, disabled, inputRef }) => (
<div className='control'>
<label htmlFor={id}>{text}</label>
<input
type='text'
id={id}
className='control-input'
disabled={disabled}
value={value}
onChange={onChange}
ref={inputRef}
/>
</div>
)
FieldInput.defaultProps = {
disabled: false,
inputRef: null,
}
FieldInput.propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
inputRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
disabled: PropTypes.bool,
}
export default FieldInput

View File

@@ -0,0 +1,38 @@
import PropTypes from 'prop-types'
import React from 'react'
import {RadioGroup} from '../form'
const FieldRadio = ({ id, text, value, onChange, disabled, options }) => (
<div className='control'>
<label htmlFor={id}>{text}</label>
<RadioGroup
name='packaging'
disabled={disabled}
selected={value}
options={options}
onChange={onChange}
/>
</div>
)
FieldRadio.defaultProps = {
disabled: false,
options: [],
}
FieldRadio.propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
options: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
text: PropTypes.string,
})
),
}
export default FieldRadio

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import {GlobalHotKeys} from 'react-hotkeys'
function HotKeys({ onSubmit, onExplore }) {
return (
<GlobalHotKeys
keyMap={{
SUBMIT: ['command+enter', 'ctrl+enter'],
EXPLORE: ['ctrl+space'],
}}
handlers={{
SUBMIT: onSubmit,
EXPLORE: onExplore,
}}
global
/>
)
}
HotKeys.propTypes = {
onSubmit: PropTypes.func.isRequired,
onExplore: PropTypes.func.isRequired,
}
export default HotKeys

View File

@@ -0,0 +1,42 @@
import React from 'react'
import Control from './Control'
import {Footer} from '../layout'
import {Placeholder} from '../form'
export default function Loading() {
return (
<>
<Control text='项目构建方式'>
<Placeholder type='radios' count={2} width='133px' />
</Control>
<Control text='开发语言'>
<Placeholder type='radios' count={3} width='73px' />
</Control>
<Control text='Spring Boot 版本'>
<Placeholder type='radios' count={5} width='105px' />
</Control>
<Control text='项目元信息' special='md'>
<div>
<div className='control'>
<Placeholder type='input' />
</div>
<div className='control'>
<Placeholder type='input' />
</div>
<div className='control'>
<Placeholder type='dropdown' />
</div>
</div>
</Control>
<Control text='组件依赖'>
<Placeholder type='tabs' count={2} />
</Control>
<Footer>
<Placeholder type='button' width='189px' />
<Placeholder type='button' width='212px' />
<Placeholder type='button' width='110px' />
</Footer>
</>
)
}

View File

@@ -0,0 +1,59 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useContext, useEffect} from 'react'
import {AppContext} from '../../reducer/App'
import {IconChevronRight} from '../icons'
const PanelMore = ({ children, fieldFocusOnOpen }) => {
const { more, dispatch } = useContext(AppContext)
useEffect(() => {
if (more && fieldFocusOnOpen) {
setTimeout(() => {
get(fieldFocusOnOpen, 'current').focus()
}, 300)
}
}, [more, fieldFocusOnOpen])
return (
<div>
<div className='more'>
<div className='wrap'>
<a
href='/'
onClick={event => {
event.preventDefault()
dispatch({
type: 'UPDATE',
payload: { more: !more },
})
}}
className={more ? 'toggle' : ''}
>
<IconChevronRight />
{!more ? '高级选项' : '高级选项'}
</a>
</div>
</div>
<div className={`panel ${more ? 'panel-active' : ''}`}>
<div className='panel-wrap'>{children}</div>
</div>
</div>
)
}
PanelMore.defaultProps = {
children: null,
fieldFocusOnOpen: null,
}
PanelMore.propTypes = {
children: PropTypes.node,
fieldFocusOnOpen: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
}
export default PanelMore

View File

@@ -0,0 +1,73 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useContext, useEffect, useState} from 'react'
import {AppContext} from '../../reducer/App'
import {IconList, IconSearch} from '../icons'
import {InitializrContext} from '../../reducer/Initializr'
const Tabs = ({ changeTab }) => {
const { dependencies: deps, dispatch, tab } = useContext(AppContext)
const { values } = useContext(InitializrContext)
const [count, setCount] = useState(0)
useEffect(() => {
setCount(
values.dependencies.filter(d => {
return get(deps, 'list', []).find(a => a.id === d).valid
}).length
)
}, [deps, values.dependencies])
const change = newTab => {
dispatch({
type: 'UPDATE',
payload: { tab: newTab },
})
setTimeout(() => {
changeTab(newTab)
})
}
return (
<div className='tab'>
<div className='tab-container'>
<a
href='/'
aria-label='Search'
onClick={event => {
event.preventDefault()
change('quicksearch')
}}
className={`quick-search ${tab === 'quicksearch' ? 'active' : ''}`}
>
<IconSearch /><span>搜索</span>
</a>
<a
href='/'
aria-label='List'
onClick={event => {
event.preventDefault()
change('list')
}}
className={`list ${tab === 'list' ? 'active' : ''}`}
>
<IconList /><span>分组列表</span>
</a>
{count > 0 && (
<>
<strong>
<span>{count}</span>
</strong>
</>
)}
</div>
</div>
)
}
Tabs.propTypes = {
changeTab: PropTypes.func.isRequired,
}
export default Tabs

View File

@@ -0,0 +1,83 @@
import get from 'lodash.get'
import React, {useContext} from 'react'
import {IconTimes} from '../icons'
import {InitializrContext} from '../../reducer/Initializr'
function Warnings() {
const { warnings, dispatch } = useContext(InitializrContext)
if (Object.keys(warnings).length > 0) {
return (
<div className='colset'>
<div className='left' />
<div className='right'>
<div className='warnings'>
<a
className='close'
href='/#'
onClick={event => {
event.preventDefault()
dispatch({
type: 'CLEAR_WARNINGS',
})
}}
>
<IconTimes />
</a>
The following attributes could not be handled:
<ul>
{get(warnings, 'project') && (
<li>
<strong>{get(warnings, 'project.value')}</strong> is not a
valid project type,{' '}
<strong>{get(warnings, 'project.select')}</strong> has been
selected.
</li>
)}
{get(warnings, 'language') && (
<li>
<strong>{get(warnings, 'language.value')}</strong> is not a
valid language,{' '}
<strong>{get(warnings, 'language.select')}</strong> has been
selected.
</li>
)}
{get(warnings, 'boot') && (
<li>
Spring Boot <strong>{get(warnings, 'boot.value')}</strong> is
not available, <strong>{get(warnings, 'boot.select')}</strong>{' '}
has been selected.
</li>
)}
{get(warnings, 'meta.java') && (
<li>
<strong>{get(warnings, 'meta.java.value')}</strong> is not a
valid Java version,{' '}
<strong>{get(warnings, 'meta.java.select')}</strong> has been
selected.
</li>
)}
{get(warnings, 'meta.packaging') && (
<li>
<strong>{get(warnings, 'meta.packaging.value')}</strong> is
not a valid packaging,{' '}
<strong>{get(warnings, 'meta.packaging.select')}</strong> has
been selected.
</li>
)}
{get(warnings, 'dependencies') && (
<li>
The following dependencies are not supported:{' '}
<strong>{get(warnings, 'dependencies.value')}</strong>.
</li>
)}
</ul>
</div>
</div>
</div>
)
}
return <></>
}
export default Warnings

View File

@@ -0,0 +1,10 @@
export { default as Control } from './Control'
export { default as FieldInput } from './FieldInput'
export { default as FieldRadio } from './FieldRadio'
export { default as FieldError } from './FieldError'
export { default as PanelMore } from './PanelMore'
export { default as Tabs } from './Tabs'
export { default as QuickSearch } from './quick-search/QuickSearch'
export { default as List } from './list/List'
export { default as Loading } from './Loading'
export { default as Warnings } from './Warnings'

View File

@@ -0,0 +1,47 @@
import get from 'lodash.get'
import React, {useContext} from 'react'
import ListGroup from './ListGroup'
import {AppContext} from '../../../reducer/App'
import {InitializrContext} from '../../../reducer/Initializr'
const List = () => {
const { dependencies, dispatch, groupsOpened } = useContext(AppContext)
const { values, dispatch: dispatchInitializr } = useContext(InitializrContext)
return (
<div className='groups'>
{get(dependencies, 'groups').map(group => {
return (
<ListGroup
group={group.group}
key={group.group}
dependencyGroup={group}
add={id => {
dispatchInitializr({
type: 'ADD_DEPENDENCY',
payload: { id },
})
}}
remove={id => {
dispatchInitializr({
type: 'REMOVE_DEPENDENCY',
payload: { id },
})
}}
toggle={id => {
dispatch({
type: 'TOGGLE_GROUP',
payload: { id },
})
}}
itemsSelected={get(values, 'dependencies', [])}
isClose={groupsOpened.indexOf(group.group) < 0}
items={group.items}
/>
)
})}
</div>
)
}
export default List

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types'
import React from 'react'
import ListItem from './ListItem'
import {IconChevronRight} from '../../icons'
function ListGroup({
group,
items,
itemsSelected,
isClose,
add,
remove,
toggle,
}) {
const toggleGroup = event => {
event.preventDefault()
toggle(group)
}
const onKeyDown = event => {
const keyPressed = event.key
if (keyPressed === 'Enter' || keyPressed === ' ') {
toggleGroup(event)
}
}
const isItemSelected = item => {
return !!itemsSelected.find(o => o === item.id)
}
return (
<div className='group'>
<div className='group-title'>
<a
href='/'
onClick={toggleGroup}
className={!isClose ? 'toggleGroupItems' : ''}
tabIndex={0}
onKeyDown={onKeyDown}
>
<IconChevronRight />
{group}
</a>
</div>
{!isClose && (
<div className='group-items' key={`links${group}`}>
{items.map(item => (
<ListItem
key={item.id}
id={item.id}
name={item.name}
description={item.description}
valid={item.valid}
message={item.message}
selected={isItemSelected(item)}
onChange={value => {
if (value) {
add(item.id)
} else {
remove(item.id)
}
}}
/>
))}
</div>
)}
</div>
)
}
ListGroup.propTypes = {
group: PropTypes.string.isRequired,
add: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
toggle: PropTypes.func.isRequired,
itemsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
isClose: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string.isRequired,
group: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
keywords: PropTypes.string,
name: PropTypes.string.isRequired,
valid: PropTypes.bool.isRequired,
})
).isRequired,
}
export default ListGroup

View File

@@ -0,0 +1,77 @@
import PropTypes from 'prop-types'
import React from 'react'
import {IconCheck, IconPlus, IconTimes} from '../../icons'
function ListItem({
id,
name,
description,
valid,
message,
onChange,
selected,
}) {
const toggle = event => {
event.preventDefault()
if (valid) {
onChange(!selected)
}
}
const onKeyDown = event => {
const keyPressed = event.key
if (keyPressed === 'Enter' || keyPressed === ' ') {
toggle(event)
}
}
return (
<a
href='/'
onClick={toggle}
tabIndex={!valid ? -1 : ''}
className={`${!valid ? 'invalid' : ''} ${selected ? 'checked' : ''}`}
key={id}
onKeyDown={onKeyDown}
>
<div key={`d${id}`}>
<input
type='checkbox'
value={id}
key={`ck${id}`}
checked={selected}
disabled={!valid}
onChange={() => {}}
/>
<strong key={`ck1${id}`}>{name}</strong>
<br key={`br${id}`} />
{valid && <span key={`ck2${id}`}>{description}</span>}
<span key={`ck3${id}`} className='icon'>
<IconPlus key={`ck4${id}`} />
<IconTimes key={`ck5${id}`} />
<IconCheck key={`ck6${id}`} />
</span>
{!valid && (
<span className='warning' key={`warning${id}`}>
{message}
</span>
)}
</div>
</a>
)
}
ListItem.defaultProps = {
message: '',
}
ListItem.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
valid: PropTypes.bool.isRequired,
message: PropTypes.string,
onChange: PropTypes.func.isRequired,
selected: PropTypes.bool.isRequired,
}
export default ListItem

View File

@@ -0,0 +1,75 @@
import PropTypes from 'prop-types'
import React from 'react'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {IconCheck, IconTimes} from '../../icons'
function Dependencies({ list, remove }) {
return (
<TransitionGroup
component='ul'
className='dependencies-list dependencies-list-checked'
>
{list.map(item => {
return (
<CSSTransition timeout={500} classNames='fade' key={`f${item.id}`}>
<li>
<span className='dependency-item-span'>
<a
className={`dependency-item checked ${
!item.valid ? 'invalid' : ''
}`}
href='/'
onClick={event => {
event.preventDefault()
remove(item)
}}
key={item.id}
>
<div key={`d1${item.id}`}>
<strong key={`d2${item.id}`}>{item.name}</strong>
<br key={`d3${item.id}`} />
{item.valid && (
<span key={`d4${item.id}`} className='description'>
{item.description}
</span>
)}
<span key={`d5${item.id}`} className='icon'>
<IconTimes key={`d6${item.id}`} />
<IconCheck key={`d7${item.id}`} />
</span>
{!item.valid && (
<span className='warning' key={`warning${item.id}`}>
{item.message}
</span>
)}
</div>
</a>
</span>
</li>
</CSSTransition>
)
})}
</TransitionGroup>
)
}
Dependencies.defaultProps = {
list: [],
}
Dependencies.propTypes = {
list: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
group: PropTypes.string.isRequired,
valid: PropTypes.bool.isRequired,
description: PropTypes.string.isRequired,
message: PropTypes.string,
})
),
remove: PropTypes.func.isRequired,
}
export default Dependencies

View File

@@ -0,0 +1,189 @@
import * as JsSearch from 'js-search'
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useContext, useEffect, useState} from 'react'
import Dependencies from './Dependencies'
import Result from './Result'
import {AppContext} from '../../../reducer/App'
import {InitializrContext} from '../../../reducer/Initializr'
const sortResult = dependencies => {
return dependencies.sort((a, b) => {
if (a.valid && !b.valid) {
return -1
}
if (!a.valid && b.valid) {
return 1
}
return b.weight - a.weight
})
}
const QuickSearch = ({ submit, input }) => {
const { values, dispatch } = useContext(InitializrContext)
const { dependencies: dependenciesContext } = useContext(AppContext)
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(0)
const [dependencies, setDependencies] = useState([])
const [result, setResult] = useState([])
const [count, setCount] = useState(0)
const [search, setSearch] = useState(null)
const add = id => {
dispatch({
type: 'ADD_DEPENDENCY',
payload: { id },
})
}
useEffect(() => {
const newDeps = get(values, 'dependencies', []).map(item => {
return get(dependenciesContext, 'list', []).find(d => d.id === item)
})
setDependencies(newDeps)
const jsSearchUp = new JsSearch.Search('name')
jsSearchUp.addIndex('name')
jsSearchUp.addIndex('id')
jsSearchUp.addIndex('description')
jsSearchUp.addIndex('group')
jsSearchUp.addDocuments(get(dependenciesContext, 'list'))
setSearch(jsSearchUp)
}, [values, dependenciesContext, values.dependencies])
const onFocus = () => {
setSelected(0)
}
const onBlur = () => {
setSelected(-1)
}
const onKeyDown = event => {
switch (event.keyCode) {
case 40: // Down
event.preventDefault()
setSelected(Math.min(selected + 1, result.length - 1, 4))
break
case 38: // Up
event.preventDefault()
setSelected(Math.max(selected - 1, 0))
break
case 13: // Enter
event.preventDefault()
if (result.length > 0) {
add(result[selected].id)
setQuery('')
} else {
submit()
}
break
case 27: // Escape
event.preventDefault()
setQuery('')
break
case 39: // Right
case 37: // Left
break
default:
// Default
setSelected(0)
}
}
useEffect(() => {
const onSearch = () => {
if (!search) {
return
}
let vals = search
.search(query)
.filter(
item => !get(values, 'dependencies', []).find(o => o === item.id)
)
setCount(vals.length)
if (vals.length > 5) {
vals = vals.slice(0, 5)
}
vals = sortResult(vals)
setResult(vals)
}
onSearch()
}, [values, query, search, setResult])
return (
<div className='colset-2'>
<div className='column'>
<label className='search-label' htmlFor='input-quicksearch'>
{/* eslint-disable-line */}
搜索依赖组件
</label>
<input
type='text'
className='control-input'
placeholder='Web, Security, JPA, Actuator, Devtools...'
value={query}
onBlur={onBlur}
onFocus={onFocus}
onChange={event => {
setQuery(event.target.value)
setSelected(0)
}}
id='input-quicksearch'
ref={input}
onKeyDown={onKeyDown}
/>
<Result
list={result}
add={item => {
add(item.id)
setQuery('')
if (input) {
get(input, 'current').focus()
}
}}
selected={selected}
select={setSelected}
/>
{count > 5 && (
<div className='search-more-warning'>
<p>
More than 5 results found.
<br />
Refine your search if necessary.
</p>
</div>
)}
</div>
<div className='column'>
{/* eslint-disable-next-line */}
<label>已选组件</label>
{get(values, 'dependencies', []).length === 0 ? (
<div className='search-no-selected'>暂未选择任何组件</div>
) : (
<Dependencies
list={dependencies}
remove={item => {
dispatch({
type: 'REMOVE_DEPENDENCY',
payload: { id: item.id },
})
}}
/>
)}
</div>
</div>
)
}
QuickSearch.defaultProps = {}
QuickSearch.propTypes = {
submit: PropTypes.func.isRequired,
input: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
}
export default QuickSearch

View File

@@ -0,0 +1,93 @@
import PropTypes from 'prop-types'
import React from 'react'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {IconPlus} from '../../icons'
function Result({ list, selected, select, add }) {
return (
<TransitionGroup component='ul' className='dependencies-list'>
{list.map((item, index) => {
return (
<CSSTransition timeout={500} classNames='fade' key={`f${item.id}`}>
<li>
<span className='dependency-item-span'>
<a
href='/'
className={`dependency-item dependency-item-gray ${
!item.valid ? 'invalid' : ''
} ${selected === index ? 'selected' : ''}`}
key={`item${item.id}`}
selected={selected === index}
disabled={!item.valid}
onClick={e => {
e.preventDefault()
if (item.valid) {
add(item)
}
}}
onMouseEnter={() => {
if (item.valid) {
select(index)
}
}}
onMouseLeave={() => {
if (item.valid) {
select(-1)
}
}}
>
<div>
<strong className='title' key={`item${item.id}`}>
{item.name}
</strong>
<br />
{item.valid && (
<span key={`d1${item.id}`}>
<span className='description' key={`d2${item.id}`}>
{item.description}
</span>
<span key={`d3${item.id}`} className='icon'>
<IconPlus key={`d4${item.id}`} />
</span>
</span>
)}
{!item.valid && (
<span className='warning' key={`warning${item.id}`}>
{item.message}
</span>
)}
</div>
</a>
</span>
</li>
</CSSTransition>
)
})}
</TransitionGroup>
)
// }
}
Result.defaultProps = {
list: [],
selected: null,
}
Result.propTypes = {
list: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
group: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
versionRange: PropTypes.string,
versionRequirement: PropTypes.string,
})
),
add: PropTypes.func.isRequired,
selected: PropTypes.number,
select: PropTypes.func.isRequired,
}
export default Result

View File

@@ -0,0 +1,82 @@
import Highlight from 'prism-react-renderer'
import Prism from 'prism-react-renderer/prism'
import PropTypes from 'prop-types'
import React from 'react'
import get from 'lodash.get'
const ReactMarkdown = require('react-markdown')
if (typeof global !== 'undefined') {
global.Prism = Prism
require('prismjs/components/prism-java') // eslint-disable-line
require('prismjs/components/prism-kotlin') // eslint-disable-line
require('prismjs/components/prism-properties') // eslint-disable-line
require('prismjs/components/prism-groovy') // eslint-disable-line
require('prismjs/components/prism-git') // eslint-disable-line
}
function Code({ item }) {
const code = get(item, 'content', '').replace(/\t/g, ' ')
const language = get(item, 'language')
if (language === 'markdown' && !get(item, 'force', false)) {
return (
<div className='markdown'>
<ReactMarkdown linkTarget='_blank' source={code} />
</div>
)
}
return (
<Highlight Prism={Prism} code={code} language={language} theme={null}>
{({ className, style, tokens, getLineProps, getTokenProps }) => {
let groupLine = tokens.length > 9 ? '2' : '1'
groupLine = tokens.length > 99 ? '3' : groupLine
groupLine = tokens.length > 999 ? '4' : groupLine
return (
<pre className={`${className} line-${groupLine}`} style={style}>
{tokens.map((line, i) => {
const props = getLineProps({ line, key: i })
return (
<div
key={get(props, 'key')}
className={get(props, 'className')}
>
<span data-value={i + 1} className='explorer-number' />
{line.map((token, key) => {
const props2 = getTokenProps({ token, key })
return (
<span
className={get(props2, 'className')}
style={get(props2, 'style')}
key={get(props2, 'key')}
>
{get(props2, 'children')}
</span>
)
})}
</div>
)
})}
</pre>
)
}}
</Highlight>
)
}
Code.defaultProps = {
item: {
content: '',
force: false,
language: 'md',
},
}
Code.propTypes = {
item: PropTypes.shape({
content: PropTypes.string,
force: PropTypes.bool,
language: PropTypes.string,
}),
}
export default Code

View File

@@ -0,0 +1,199 @@
import '../../../styles/explore.scss'
import FileSaver from 'file-saver'
import JSZip from 'jszip'
import Modal from 'react-responsive-modal'
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useContext, useEffect, useState} from 'react'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {toast} from 'react-toastify'
import Code from './Code'
import Loading from './Loading'
import Tree from './Tree'
import {AppContext} from '../../reducer/App'
import {IconFile, IconTimes} from '../icons'
import {createTree, findRoot} from '../../utils/Zip'
function Explore({ open, onClose, projectName, blob }) {
const [button, setButton] = useState('Copy')
const [tree, setTree] = useState(null)
const [selected, setSelected] = useState(null)
const { dispatch, explore } = useContext(AppContext)
useEffect(() => {
const load = async () => {
try {
const zipJs = new JSZip()
const { files } = await zipJs.loadAsync(blob).catch(() => {
throw Error(`Could not load the ZIP project.`)
})
const path = `${findRoot({ files })}/`
const result = await createTree(files, path, path, zipJs).catch(() => {
throw Error(`Could not read the ZIP project.`)
})
setSelected(result.selected)
setTree(result.tree)
} catch (e) {
dispatch({ type: 'EXPLORE_UPDATE', payload: { open: false } })
toast.error(e.message)
}
}
if (explore && blob) {
load()
}
}, [explore, blob, dispatch])
const onCopy = () => {
setButton('Copied!')
setTimeout(() => {
setButton('Copy!')
}, 3000)
}
const download = file => {
const blobFile = new Blob([file.content], {
type: 'text/plain;charset=utf-8',
})
FileSaver.saveAs(blobFile, file.filename)
}
const downloadZip = () => {
FileSaver.saveAs(blob, projectName)
}
return (
<div>
<Modal
open={open}
onClose={() => {
setSelected(null)
onClose()
}}
showCloseIcon={false}
classNames={{ modal: 'modal-explorer', overlay: 'overlay' }}
>
{tree && selected ? (
<div className='colset-explorer'>
<div className='left'>
<div className='head'>
<strong>{projectName}</strong>
</div>
<div className='explorer-content'>
<Tree
selected={selected}
onClickItem={item => {
setSelected(item)
// onSelected(item)
}}
tree={tree}
/>
</div>
<div className='foot'>
<a
href='/#'
onClick={e => {
e.preventDefault()
downloadZip()
}}
className='action'
>
下载源码包
</a>
</div>
</div>
<div className='right'>
{selected && (
<>
<div className='head'>
<strong>
<IconFile />
{get(selected, 'filename')}
</strong>
<div className='actions'>
<span className='divider'>|</span>
<a
href='/#'
onClick={e => {
e.preventDefault()
download(selected)
}}
className='action'
>
Download
</a>
<span className='divider'>|</span>
<CopyToClipboard
onCopy={onCopy}
text={get(selected, 'content', '')}
>
<a
href='/#'
onClick={e => {
e.preventDefault()
}}
className='action'
>
{button}
</a>
</CopyToClipboard>
{get(selected, 'language') === 'markdown' && (
<>
<span className='divider'>|</span>
<a
href='/#'
onClick={e => {
e.preventDefault()
const newSelected = { ...selected }
newSelected.force = !get(selected, 'force', false)
setSelected(newSelected)
}}
className='action'
>
{get(selected, 'force', false)
? 'Preview'
: 'View source'}
</a>
</>
)}
</div>
<a
href='/#'
onClick={e => {
e.preventDefault()
onClose()
}}
className='close'
>
<IconTimes />
</a>
</div>
<div className='explorer-content'>
<Code item={selected} onChange={() => {}} />
</div>
</>
)}
</div>
</div>
) : (
<Loading onClose={onClose} />
)}
</Modal>
</div>
)
}
Explore.defaultProps = {
projectName: '',
blob: null,
}
Explore.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
projectName: PropTypes.string,
blob: PropTypes.instanceOf(Blob),
}
export default Explore

View File

@@ -0,0 +1,84 @@
import PropTypes from 'prop-types'
import React from 'react'
import {IconTimes} from '../icons'
import {Placeholder} from '../form'
function Loading({ onClose }) {
return (
<>
<a
href='/#'
onClick={e => {
e.preventDefault()
onClose()
}}
>
{' '}
</a>
<div className='colset-explorer'>
<div className='left'>
<div className='head'>
<Placeholder width='70px' type='text' />
</div>
<div className='explorer-content'>
<ul className='explorer-ul-placeholder'>
<li>
<Placeholder type='text' width='66px' />
</li>
<li>
<Placeholder type='text' width='60px' />
</li>
<li>
<Placeholder type='text' width='45px' />
</li>
<li>
<Placeholder type='text' width='87px' />
</li>
<li>
<Placeholder type='text' width='80px' />
</li>
<li>
<Placeholder type='text' width='94px' />
</li>
<li>
<Placeholder type='text' width='86px' />
</li>
</ul>
</div>
<div className='foot'>
<Placeholder type='text' width='120px' />
</div>
</div>
<div className='right'>
<>
<div className='head'>
<Placeholder width='86px' type='text' />
<div className='actions'>
<Placeholder width='74px' type='text' />
<Placeholder width='43px' type='text' />
</div>
<a
href='/#'
onClick={e => {
e.preventDefault()
onClose()
}}
className='close'
>
<IconTimes />
</a>
</div>
<div className='content' />
</>
</div>
</div>
</>
)
}
Loading.propTypes = {
onClose: PropTypes.func.isRequired,
}
export default Loading

View File

@@ -0,0 +1,122 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useEffect, useState} from 'react'
import {IconCaretDown, IconFile, IconFolder} from '../icons'
function Tree({ tree, selected, onClickItem }) {
const [folders, setFolders] = useState([])
useEffect(() => {
const treeToArray = map => {
const recursive = (mapRec, acc) => {
mapRec.forEach(item => {
if (item.type === 'folder') {
acc.push({
filename: get(item, 'filename'),
path: get(item, 'path'),
hidden: get(item, 'hidden', false),
})
if (get(item, 'children')) {
recursive(item.children, acc)
}
}
})
return acc
}
return recursive(map, [])
}
setFolders(treeToArray(tree.children))
}, [tree, setFolders])
const renderItem = (item, depth = 0) => {
if (item.type === 'folder') {
const folder = folders.find(f => f.path === item.path)
const isHidden = get(folder, `hidden`, true)
return (
<li
key={`li${item.path}`}
className={`li-folder ${isHidden ? 'folder-hide' : ''}`}
>
<a
href='/#'
key={`s1${item.path}`}
className={`folder level-${depth}`}
onClick={e => {
e.preventDefault()
const newFolders = [...folders]
const newFolder = newFolders.find(f => f.path === item.path)
if (newFolder) {
newFolder.hidden = !get(newFolder, `hidden`, true)
setFolders(newFolders)
}
}}
>
<span key={`s2${item.path}`} className='text'>
{get(item, 'children.length', 0) > 0 && <IconCaretDown />}
<span key={`s3${item.path}`} className='icon'>
<IconFolder key={`s4${item.path}`} />
</span>
{item.filename}
</span>
</a>
{get(item, 'children') && (
<ul className='ul' key={`ul${item.path}`}>
{item.children.map(it => renderItem(it, depth + 1))}
</ul>
)}
</li>
)
}
// File
const isDisabled = get(item, 'language') === null
const isSelected = get(selected, 'path') === get(item, 'path')
return (
<li key={`li${item.path}`} className='li-file'>
<a
href='/#'
key={`s1${item.path}`}
tabIndex={`${isDisabled ? -1 : ''}`}
className={`file level-${depth} ${isDisabled ? 'disabled' : ''} ${
isSelected ? 'selected' : ''
}`}
onClick={e => {
e.preventDefault()
if (!isDisabled) {
onClickItem(item)
}
}}
>
<span key={`s2${item.path}`} className='text'>
<span key={`s3${item.path}`} className='icon'>
<IconFile key={`s4${item.path}`} />
</span>
{item.filename}
</span>
</a>
</li>
)
}
return (
<ul className='explorer-ul'>
{tree.children.map(item => renderItem(item, 0))}
</ul>
)
}
Tree.propTypes = {
tree: PropTypes.shape({
children: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
})
),
}).isRequired,
selected: PropTypes.shape({
path: PropTypes.string.isRequired,
}).isRequired,
onClickItem: PropTypes.func.isRequired,
}
export default Tree

View File

@@ -0,0 +1 @@
export { default as Explore } from './Explore'

View File

@@ -0,0 +1,100 @@
import '../../../styles/fetch.scss'
import get from 'lodash.get'
import PropTypes from 'prop-types'
import React, {useContext, useRef, useState,} from 'react'
import {toast} from 'react-toastify'
import Modal from 'react-responsive-modal'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import FileSaver from 'file-saver'
import useWindowsUtils from '../../utils/WindowsUtils'
import {InitializrContext} from '../../reducer/Initializr'
import {getProject, getQueryString} from '../../utils/ApiUtils'
import {AppContext} from '../../reducer/App'
function Fetch({ open, onClose }) {
const windowsUtils = useWindowsUtils()
const origin = `${windowsUtils.origin}`
const { values } = useContext(InitializrContext)
const { dependencies } = useContext(AppContext)
const config = get(dependencies, 'list')
const params = getQueryString(values, config)
const gitUrl = 'git clone "' + `${origin}/${params}/${values.meta.artifact}.git` + '" ' + `${values.meta.artifact}`
const downloadUrl = `${origin}/starter.zip?${params}`
const [button, setButton] = useState('复制')
const input = useRef(null)
const onCopy = () => {
setButton('已复制!')
setTimeout(() => {
onClose()
setButton('复制')
}, 500)
}
const onDownload = async () => {
const url = `${windowsUtils.origin}/starter.zip`
const project = await getProject(
url,
values,
get(dependencies, 'list')
).catch(() => {
toast.error(`Could not connect to server. Please check your network.`)
})
FileSaver.saveAs(project, `${get(values, 'meta.artifact')}.zip`)
onClose()
setButton('复制')
}
return (
<div>
<Modal
open={open}
onClose={() => {
onClose()
setButton('复制')
}}
classNames={{ modal: 'modal-fetch', overlay: 'overlay' }}
center
>
<div>
<h2>获取完整项目代码</h2>
<div>
<div class="fetch-method control">
<span>下载代码包</span>
<input class="control-input" value={downloadUrl} readonly />
<a
href='/#'
onClick={e => {
e.preventDefault()
onDownload()
}}
>下载</a>
</div>
<div class="fetch-method control">
<span>Git Clone 命令</span>
<input class="control-input" value={gitUrl} readonly />
<CopyToClipboard onCopy={onCopy} text={gitUrl}>
<a
href='/#'
onClick={e => {
e.preventDefault()
}}
>{button}</a>
</CopyToClipboard>
</div>
</div>
</div>
</Modal>
</div>
)
}
Fetch.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
}
export default Fetch

View File

@@ -0,0 +1,42 @@
import PropTypes from 'prop-types'
import React from 'react'
function Button({ id, onClick, children, variant, hotkey }) {
return (
<button
className={`button ${variant === 'primary' ? 'primary' : ''}`}
type='button'
id={id}
onClick={event => {
if (onClick) {
onClick(event)
}
}}
>
{children}
{hotkey && (
<>
{' '}
<span className='desktop-only'>- {hotkey}</span>
</>
)}
</button>
)
}
Button.defaultProps = {
onClick: null,
children: null,
variant: '',
hotkey: '',
}
Button.propTypes = {
id: PropTypes.string.isRequired,
variant: PropTypes.string,
hotkey: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.node,
}
export default Button

View File

@@ -0,0 +1,29 @@
import PropTypes from 'prop-types'
import React from 'react'
import {IconTimes} from '../icons'
const Close = ({ onClose }) => (
<a
href='/#'
className='toast-close'
onClick={event => {
event.preventDefault()
if (onClose) {
onClose()
}
}}
>
<IconTimes />
</a>
)
Close.defaultProps = {
onClose: null,
}
Close.propTypes = {
onClose: PropTypes.func,
}
export default Close

View File

@@ -0,0 +1,29 @@
import PropTypes from 'prop-types'
import React from 'react'
const Form = ({ onSubmit, children }) => (
<form onSubmit={onSubmit} autoComplete='off'>
<input
style={{ display: 'none' }}
type='text'
name='fakeusernameremembered'
/>
<input
style={{ display: 'none' }}
type='password'
name='fakepasswordremembered'
/>
{children}
</form>
)
Form.defaultProps = {
children: null,
}
Form.propTypes = {
onSubmit: PropTypes.func.isRequired,
children: PropTypes.node,
}
export default Form

View File

@@ -0,0 +1,49 @@
import PropTypes from 'prop-types'
import React from 'react'
function Placeholder({ type, count, width }) {
if (type === 'radios') {
const options = Array.from({ length: count }, (item, i) => {
const id = `p${i}`
return <span key={id} style={{ width }} className='placeholder-radio' />
})
return <span className='placeholder-radios'>{options}</span>
}
if (type === 'input') {
return <span className='placeholder-input' />
}
if (type === 'dropdown') {
return <span className='placeholder-dropdown' />
}
if (type === 'text') {
return <span style={{ width }} className='placeholder-text' />
}
if (type === 'button') {
return <span style={{ width }} className='placeholder-button' />
}
if (type === 'tabs') {
const options = Array.from({ length: count }, (item, i) => {
return <span key={`p${i}`} className='placeholder-tab' />
})
return (
<span className='placeholder-tabs'>
<span className='placeholder-header'>{options}</span>
</span>
)
}
return ''
}
Placeholder.defaultProps = {
width: '150px',
type: 'radios',
count: 3,
}
Placeholder.propTypes = {
type: PropTypes.string,
count: PropTypes.number,
width: PropTypes.string,
}
export default Placeholder

View File

@@ -0,0 +1,45 @@
import PropTypes from 'prop-types'
import React from 'react'
function Radio({ handler, value, disabled, error, checked, text }) {
const onClick = event => {
event.preventDefault()
handler(value)
}
if (disabled || error) {
return (
<span
className={`radio disabled ${checked ? 'checked' : ''} ${
error ? 'err' : ''
}`}
>
{text}
</span>
)
}
return (
<a
href='/'
className={`radio ${checked ? 'checked' : ''}`}
onClick={onClick}
>
{text}
</a>
)
}
Radio.defaultProps = {
disabled: false,
error: false,
}
Radio.propTypes = {
checked: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
handler: PropTypes.func.isRequired,
disabled: PropTypes.bool,
error: PropTypes.bool,
}
export default Radio

View File

@@ -0,0 +1,63 @@
import PropTypes from 'prop-types'
import React from 'react'
import Radio from './Radio'
function RadioGroup({ onChange, options, error, selected, disabled }) {
const onChangeHandler = value => {
if (onChange) {
onChange(value)
}
}
const allOptions = options.map(option => {
return (
<Radio
key={option.key}
checked={!error && selected === option.key}
text={option.text}
value={option.key}
disabled={disabled}
handler={onChangeHandler}
/>
)
})
if (error) {
allOptions.push(
<Radio
key={allOptions.length + 1}
checked
text={error}
value={error}
disabled={disabled}
handler={onChangeHandler}
error
/>
)
}
return <div className='group-radio'>{allOptions}</div>
}
RadioGroup.defaultProps = {
selected: '',
error: null,
onChange: null,
disabled: false,
options: {
error: '',
},
}
RadioGroup.propTypes = {
selected: PropTypes.string,
error: PropTypes.string,
options: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
})
),
onChange: PropTypes.func,
disabled: PropTypes.bool,
}
export default RadioGroup

View File

@@ -0,0 +1,33 @@
import PropTypes from 'prop-types'
import React from 'react'
const Switch = ({ id, isOn, onChange }) => {
return (
<span className='switch'>
<input
checked={isOn}
onChange={onChange}
className='switch-checkbox'
id={id}
name='switch-new'
type='checkbox'
/>
{/* eslint-disable-next-line */}
<label className='switch-label' htmlFor={id}>
<span className='switch-button' />
</label>
</span>
)
}
Switch.defaultProps = {
isOn: false,
onChange: null,
}
Switch.propTypes = {
isOn: PropTypes.bool,
onChange: PropTypes.func,
}
export default Switch

View File

@@ -0,0 +1,7 @@
export { default as Radio } from './Radio'
export { default as RadioGroup } from './RadioGroup'
export { default as Switch } from './Switch'
export { default as Close } from './Close'
export { default as Placeholder } from './Placeholder'
export { default as Form } from './Form'
export { default as Button } from './Button'

View File

@@ -0,0 +1,22 @@
import React from 'react'
export const IconSpring = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='spring'
role='img'
xmlns='http://www.w3.org/2000/svg'
className='icon-spring'
viewBox='0 0 248.7 248.38'
>
<g>
<g>
<path
d='M226.28,14.15a114.05,114.05,0,0,1-13.22,23.44A124.1,124.1,0,1,0,39.75,215.14l2.72,2.4A10.61,10.61,0,1,1,57.39,216a10.62,10.62,0,0,1-14.92,1.55l1.85,1.64a123.56,123.56,0,0,0,79.86,29.19c65.45,0,119.21-50.94,123.81-115.24C251.4,101.66,242.1,61.69,226.28,14.15Zm-.53,164.69c-30.61,40.78-96,27-137.92,29,0,0-7.42.42-14.89,1.65,0,0,2.83-1.21,6.43-2.45,29.44-10.19,43.35-12.23,61.25-21.42,33.61-17.22,67.06-54.72,73.87-93.69-12.8,37.48-51.71,69.73-87.14,82.82-24.25,9-68.11,17.66-68.11,17.67s-1.75-.92-1.78-.94C27.64,177,26.78,112.36,81,91.54c23.75-9.14,46.47-4.12,72.14-10.23,27.38-6.51,59.1-27,72-53.88C239.53,70.3,256.89,137.36,225.75,178.84Z'
fill='currentColor'
/>
</g>
</g>
</svg>
)

View File

@@ -0,0 +1,339 @@
import React from 'react'
export const IconCaretDown = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='caret-down'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 320 512'
className='icon-caret-down'
>
<path
fill='currentColor'
d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'
/>
</svg>
)
export const IconCheck = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='check'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
className='icon-check'
>
<path
fill='currentColor'
d='M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z'
/>
</svg>
)
export const IconFolder = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='folder'
role='img'
xmlns='http://www.w3.org/2000/svg'
className='icon-folder'
viewBox='0 0 512 512'
>
<g>
<path
fill='#86ad5c'
d='M430.1,192H81.9c-17.7,0-18.6,9.2-17.6,20.5l13,183c0.9,11.2,3.5,20.5,21.1,20.5h316.2c18,0,20.1-9.2,21.1-20.5l12.1-185.3 C448.7,199,447.8,192,430.1,192z'
/>
<g>
<path
fill='#718c50'
d='M426.2,143.3c-0.5-12.4-4.5-15.3-15.1-15.3c0,0-121.4,0-143.2,0c-21.8,0-24.4,0.3-40.9-17.4C213.3,95.8,218.7,96,190.4,96 c-22.6,0-75.3,0-75.3,0c-17.4,0-23.6-1.5-25.2,16.6c-1.5,16.7-5,57.2-5.5,63.4h343.4L426.2,143.3z'
/>
</g>
</g>
</svg>
)
export const IconFile = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='file'
role='img'
xmlns='http://www.w3.org/2000/svg'
className='icon-file'
viewBox='0 0 1792 1792'
>
<path
fill='currentColor'
d='M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280z'
/>
</svg>
)
export const IconChevronLeft = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='chevron-left'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 320 512'
className='icon-chevron-left'
>
<path
fill='currentColor'
d='M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z'
/>
</svg>
)
export const IconChevronRight = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='chevron-right'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 320 512'
className='icon-chevron-right'
>
<path
fill='currentColor'
d='M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z'
/>
</svg>
)
export const IconChevronDown = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='chevron-down'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 448 512'
className='icon-chevron-down'
>
<path
fill='currentColor'
d='M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z'
/>
</svg>
)
export const IconChevronUp = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='chevron-up'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 448 512'
className='icon-chevron-up'
>
<path
fill='currentColor'
d='M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z'
/>
</svg>
)
export const IconGithub = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='github'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 496 512'
className='icon-github'
>
<path
fill='currentColor'
d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'
/>
</svg>
)
export const IconList = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='list'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
className='icon-list'
>
<path
fill='currentColor'
d='M128 116V76c0-8.837 7.163-16 16-16h352c8.837 0 16 7.163 16 16v40c0 8.837-7.163 16-16 16H144c-8.837 0-16-7.163-16-16zm16 176h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zM16 144h64c8.837 0 16-7.163 16-16V64c0-8.837-7.163-16-16-16H16C7.163 48 0 55.163 0 64v64c0 8.837 7.163 16 16 16zm0 160h64c8.837 0 16-7.163 16-16v-64c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v64c0 8.837 7.163 16 16 16zm0 160h64c8.837 0 16-7.163 16-16v-64c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v64c0 8.837 7.163 16 16 16z'
/>
</svg>
)
export const IconPlus = () => (
<svg
aria-hidden='true'
focusable='false'
data-prefix='fas'
data-icon='plus'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 448 512'
className='icon-plus'
>
<path
fill='currentColor'
d='M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z'
/>
</svg>
)
export const IconSearch = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='search'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
className='icon-search'
>
<path
fill='currentColor'
d='M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z'
/>
</svg>
)
export const IconTimes = () => (
<svg
aria-hidden='true'
focusable='false'
data-prefix='fas'
data-icon='times'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 352 512'
className='icon-times'
>
<path
fill='currentColor'
d='M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z'
/>
</svg>
)
export const IconTwitter = () => (
<svg
aria-hidden='true'
focusable='false'
data-icon='twitter'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
className='icon-twitter'
>
<path
fill='currentColor'
d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'
/>
</svg>
)
export const IconHelp = () => (
<svg
t="1583993417464"
className='icon-help'
focusable='false'
aria-hidden='true'
role='img'
data-icon='help'
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1960"
width="18"
height="18">
<path
p-id="1961"
fill='currentColor'
d="M0.12992 518.398052C-6.268452 236.869666 224.072954 6.528259 505.601341 0.129887c281.528386-6.398372 511.869793 223.943034 518.268165 505.471421 6.398372 281.528386-223.943034 511.869793-505.47142 518.268165-281.528386 6.398372-511.869793-223.943034-518.268166-505.471421zM595.178555 767.934577c0-44.788607-38.390234-83.178841-83.178842-83.178842-44.788607 0-83.178841 38.390234-83.178841 83.178842 0 44.788607 38.390234 83.178841 83.178841 83.178841 44.788607 0 83.178841-38.390234 83.178842-83.178841zM300.853423 345.641997v12.796745c0 31.991862 6.398372 63.983724 63.983725 63.983724S435.219244 384.032232 435.219244 345.641997c0-12.796745 38.390234-44.788607 76.780469-44.788607s83.178841 19.195117 83.178842 76.780469c0 38.390234-63.983724 76.780469-95.975587 108.772331-70.382097 63.983724-63.983724 89.577214-63.983724 89.577214 0 38.390234 12.796745 76.780469 70.382097 70.382097 57.585352-6.398372 166.357683-153.560938 166.357682-153.560938 51.186979-57.585352 51.186979-127.967448 51.18698-127.967448 0-121.569076-95.975586-185.5528-211.14629-185.5528-115.170703-6.398372-204.747917 51.186979-211.14629 166.357682z">
</path>
</svg>
)
export const IconFeedBack = () => (
<svg
t="1589424011678"
class="icon-feedback"
focusable='false'
aria-hidden='true'
viewBox="0 0 1070 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1374"
width="32"
height="32">
<path
fill='currentColor'
p-id="1375"
d="M1021.713308 633.948781a48.653015 48.653015 0 0 1-48.653014-48.653014l-0.389224-487.673493-728.578895 0.413551a48.653015 48.653015 0 0 1 0-97.30603l678.709554-0.389224v0.413551h51.693829a97.306029 97.306029 0 0 1 95.359908 77.844823L1070.366323 585.295767a48.653015 48.653015 0 0 1-48.653015 48.653014zM924.407279 229.544923l0.413551 307.584359c0 1.216325-0.267592 2.432651-0.364898 3.600323l0.291918 200.912624a96.965458 96.965458 0 0 1-96.138357 86.213142h-51.693828l-270.024232 0.316245-182.448805 181.086521a48.653015 48.653015 0 1 1-68.795362-68.795363l189.162921-187.703331a48.653015 48.653015 0 0 1 40.576614-21.893856h1.946121l340.157552-0.316245v-76.506866 10.217133-17.75835L827.10125 486.5788c0-1.289305 0.267592-2.432651 0.364897-3.721956l-0.218938-164.155271c-1.021713-64.07602-9.000808-73.247114-64.659857-74.487766l64.173326 0.437877 0.486531 70.668504L827.10125 244.019195h-71.665891 7.10334-15.495985l-587.314867 0.316245c-51.085665 1.727182-60.645983 10.922602-62.397491 57.434884l0.291918 382.388368c2.432651 38.533188 14.085048 45.101345 65.851855 46.220364l-65.827529-0.437877v0.997387l194.903977-0.340571a48.653015 48.653015 0 0 1 0 97.306029l-143.526393 0.243265v-0.291918H97.306029a97.111417 97.111417 0 0 1-96.892478-93.073217L0 243.484012a97.062764 97.062764 0 0 1 92.927258-96.332969l736.120112-0.437877A96.989785 96.989785 0 0 1 924.407279 229.544923z m-827.10125 14.912149v33.424621l0.218939-33.230009 31.259562-0.218938H97.306029z m669.708747 485.873331c51.085665-1.240652 59.040433-9.730603 60.18378-65.997814l-0.462204 65.68157z m0 0c-10.460398 0.243265-22.502019 0.243265-36.927638 0.243265z m-603.51632 0l32.281275 0.218939c-12.211907 0-22.84259 0-32.232622-0.194612z m741.958474-632.099966l65.803202 0.437877 0.364898 54.613009c-1.483917-46.512282-10.72799-53.90754-66.192427-55.02656z m0 0l-32.402908-0.170286c12.284886 0 22.939896 0 32.378581 0.194612z m66.435691 97.135743l-0.291918-42.036204c0.364898 11.482111 0.291918 25.202262 0.291918 42.036204z">
</path>
</svg>
)
export const IconCloudTookit = () => (
<svg
t="1587979013746"
className='icon-cloud-tookit'
focusable='false'
aria-hidden='true'
role='img'
data-icon='help'
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="679"
width="25"
height="25">
<path
p-id="680"
fill='currentColor'
d="M228.616 662.377c-18.254-4.236-32.293-19.763-32.293-39.526V405.455c1.404-19.764 14.041-35.291 32.293-39.527l200.78-43.76 21.062-86.112H217.384c-82.839 0-148.83 66.347-148.83 148.225v256.923c0 81.876 67.395 149.636 148.83 149.636h233.074l-21.061-86.112-200.781-42.351z m584.092-426.323H578.226l21.062 86.111 200.785 43.761c18.253 4.237 32.287 19.764 32.287 39.526V622.85c-1.4 19.763-14.035 35.29-32.287 39.526l-200.785 43.762-21.062 86.11h234.482c81.436 0 148.83-66.347 148.83-149.635V384.278c-1.41-81.877-67.394-148.224-148.83-148.224zM429.396 502.86H600.69v21.174H429.396V502.86z">
</path>
</svg>
)
export const IconInitialzrTutorial = () => (
<svg t="1603192889200" class="icon-cloud-tookit" viewBox="0 0 1376 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1399" width="200" height="200" style={{left: '20px'}}>
<path
d="M596.800316 1023.986434q-3.66285 0-7.447794-0.217058c-24.581792-1.356611-50.303136-9.048595-80.04005-17.934397-110.238211-32.789288-153.134251-29.397761-187.565038-26.630275-51.008574 4.069833-91.842566 15.926613-127.88772 26.345386-56.177262 16.279332-104.689672 30.293124-147.029502-6.783055C18.802629 974.239508 3.065941 934.6129 0.054264 880.972501V186.04851c0-91.313487 68.49529-165.750733 153.161384-167.093778a1093.645533 1093.645533 0 0 1 405.463899 0 151.831904 151.831904 0 0 1 108.094765 49.774058c28.990777 31.554772 44.958089 73.256995 44.958089 117.31972V879.683721c-3.147338 53.002792-18.870459 92.819325-46.748815 118.36431-19.00612 17.418885-41.403768 25.938403-68.18327 25.938403z m-243.376015-93.60616c69.187162 0 124.021379 16.360729 169.400017 29.845442 57.561005 17.161129 86.294026 24.418998 110.034719 2.713222 18.354947-16.821977 28.922947-45.58213 31.378413-85.466493V186.04851c0-32.165247-11.517627-62.404107-32.43657-85.195171a105.476506 105.476506 0 0 0-75.631063-34.430788h-2.19771l-2.157012-0.406983a1046.245545 1046.245545 0 0 0-391.884223 0l-2.19771 0.420549h-2.238408c-28.326038 0-55.159804 12.209499-75.563233 34.444354-20.918942 22.763933-32.436569 53.016358-32.436569 85.195171v692.943339c2.333371 40.060723 12.630049 68.346063 30.591578 84.109883 10.337376 9.048595 21.488718 12.52152 37.320369 11.626156 17.635943-1.003892 39.72157-7.379964 65.239423-14.759928 36.302911-10.513735 81.491623-23.591465 137.275469-28.068281 12.236631-1.031024 24.066279-1.478706 35.516076-1.478706z"
p-id="1400" fill='currentColor'></path>
<path
d="M1261.064898 1023.986434c-2.4419 0-4.924498-0.067831-7.447794-0.217058-24.581792-1.356611-50.303136-9.048595-80.04005-17.934397-110.251777-32.789288-153.174949-29.397761-187.605737-26.630275-51.008574 4.069833-91.842566 15.926613-127.88772 26.345386-56.177262 16.279332-104.689672 30.293124-147.029501-6.783055-28.027584-24.527527-43.764271-64.154135-46.775948-117.794534l47.413555-2.713222c2.26554 40.521971 12.575784 69.010802 30.605144 84.828887 10.337376 9.048595 21.502285 12.52152 37.320369 11.626156 17.635943-1.003892 39.72157-7.379964 65.239424-14.759928 36.302911-10.513735 81.491623-23.591465 137.275468-28.068282 86.199064-6.932282 152.02183 12.697879 204.916094 28.488832 57.561005 17.161129 86.294026 24.418998 110.034719 2.713222 18.354947-16.821977 28.922947-45.58213 31.378412-85.466494V186.04851c0-32.165247-11.517627-62.404107-32.436569-85.195171a105.476506 105.476506 0 0 0-75.64463-34.430788h-2.19771l-2.157011-0.406983a1046.245545 1046.245545 0 0 0-391.884223 0l-2.19771 0.420549h-2.238408c-28.326038 0-55.159804 12.209499-75.563234 34.444354-20.918942 22.763933-32.436569 53.016358-32.436569 85.195171h-47.481385c0-91.313487 68.49529-165.750733 153.161383-167.093778a1093.645533 1093.645533 0 0 1 405.463899 0 151.831904 151.831904 0 0 1 108.216861 49.746926c28.990777 31.554772 44.958089 73.256995 44.958088 117.31972V879.683721c-3.147338 53.002792-18.870459 92.819325-46.748815 118.36431-19.087517 17.418885-41.444466 25.938403-68.210402 25.938403zM121.362421 229.43293h334.974391v47.481386H121.362421zM121.362421 384.507135h334.974391v47.481386H121.362421z"
p-id="1401" fill='currentColor'></path>
<path
d="M942.342707 431.988521a23.740693 23.740693 0 0 1-23.740693-23.740693V14.681407h47.481386v342.788471l60.626946-50.655856a23.740693 23.740693 0 0 1 30.442351 0l55.376862 46.260436V23.689304h47.481385V403.87954a23.740693 23.740693 0 0 1-38.961868 18.219286l-79.131121-66.12122-84.367638 70.543772a23.740693 23.740693 0 0 1-15.20761 5.467143z"
p-id="1402" fill='currentColor'></path>
</svg>
)
export const IconHot = () => (
<svg t="1603192889200" class="icon-cloud-tookit" viewBox="0 0 1376 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1399" width="200" height="200">
p-id="2096" width="200" height="200">
<path
d="M498.463465 545.489466c-32.862322 0-49.168055 24.584027-49.168055 61.460069 0 41.015189 16.431161 61.460069 49.168055 61.460068 32.862322 0 49.168055-20.44488 49.168055-61.460068-4.013719-36.876041-16.305732-57.44635-49.168055-61.460069z m0 0"
fill="#FF0000" p-id="2097"></path>
<path
d="M674.816233 164.060755c20.44488 49.168055 24.584027 123.045566-16.431161 164.060754C588.646709 57.44635 412.29394 0 412.29394 0c20.570309 139.476727-73.752082 291.245468-168.074473 406.012739-4.139147-57.44635-8.152866-94.322391-36.876041-147.629593-8.152866 102.475257-82.030377 184.505634-106.614405 282.967172-16.431161 86.169525-4.139147 164.060755 57.320921 233.799119 98.461538 94.322391 352.705537 213.228809 611.088682 4.139147 237.812837-242.077413-36.876041-574.212641-94.322391-615.227829zM379.557047 705.411073h-45.154337v-77.89123H252.372333V705.411073h-45.154336V512.627144h45.154336v77.891229h86.169525v-77.891229h45.154336V705.411073h-4.139147z m118.906418 4.139147c-61.460069-4.139147-94.322391-36.876041-98.461538-98.461538 4.139147-65.599216 36.876041-98.461538 98.461538-102.475257 61.460069 4.139147 90.183244 36.876041 94.322391 102.475257 0 61.460069-32.862322 94.322391-94.322391 98.461538z m221.507104-160.047035v159.921607h-45.154336V549.503185h-61.460068v-36.876041H781.556067v36.876041h-61.585498z m0 0"
fill="#FF0000" p-id="2098"></path>
</svg>
)

View File

@@ -0,0 +1,21 @@
export { IconTimes } from './Icons'
export { IconPlus } from './Icons'
export { IconGithub } from './Icons'
export { IconTwitter } from './Icons'
export { IconCaretDown } from './Icons'
export { IconList } from './Icons'
export { IconSearch } from './Icons'
export { IconCheck } from './Icons'
export { IconChevronLeft } from './Icons'
export { IconChevronRight } from './Icons'
export { IconChevronUp } from './Icons'
export { IconChevronDown } from './Icons'
export { IconFolder } from './Icons'
export { IconFile } from './Icons'
export { IconHelp } from './Icons'
export { IconCloudTookit } from './Icons'
export { IconInitialzrTutorial } from './Icons'
export { IconFeedBack } from './Icons'
export { IconHot } from './Icons'
export { IconSpring } from './IconSpring'

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types'
import React from 'react'
const Footer = ({ children }) => (
<div className='sticky'>
<div className='colset colset-submit'>
<div className='left nopadding'>
<footer className='footer'>
<div className='footer-container'>
© 1999-{new Date().getFullYear()} Aliyun.com
<br />
start.aliyun.com is powered by
<br />
<span>
<a
tabIndex='-1'
target='_blank'
rel='noopener noreferrer'
href='https://www.aliyun.com'
>
Aliyun.com
</a>
</span>
</div>
</footer>
</div>
{children && (
<div className='right nopadding'>
<div className='submit'>{children}</div>
</div>
)}
</div>
</div>
)
Footer.defaultProps = {
children: null,
}
Footer.propTypes = {
children: PropTypes.node,
}
export default Footer

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
const Header = ({ children }) => (
<div className='header'>
<h1 className='logo'>
<a href='/bootstrap.html'>
<span className='title'>
<strong>云原生</strong>
</span>
<span className='description'>Cloud Native App Initializer</span>
</a>
</h1>
{children}
</div>
)
Header.defaultProps = {
children: null,
}
Header.propTypes = {
children: PropTypes.node,
}
export default Header

View File

@@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import React from 'react'
import Header from './Header'
import QuickLinks from './QuickLinks'
const Layout = ({ children }) => (
<>
<Header>
<QuickLinks />
</Header>
<main>{children}</main>
</>
)
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout

View File

@@ -0,0 +1,30 @@
import PropTypes from 'prop-types'
import React from 'react'
const Logo = ({ className }) => {
return (
<svg
t="1583838227729"
className={className}
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6979" width="65" height="65">
<path
fill="#FF6A00"
p-id="6980"
d="M853.333333 852.48h-225.706666l54.613333-77.226667 164.693333-50.346666c29.866667-9.386667 49.92-37.12 49.92-68.266667V410.026667c0-31.146667-20.053333-58.88-49.493333-68.266667L682.666667 291.413333l-54.613334-77.226666H853.333333c94.293333 0 170.666667 76.373333 170.666667 170.666666v297.386667c0 93.866667-76.373333 170.24-170.666667 170.24zM341.333333 492.8h341.333334v76.8H341.333333v-76.8zM177.066667 341.333333c-29.866667 9.386667-49.92 37.12-49.493334 68.266667v247.04c0 31.146667 20.053333 58.88 49.493334 68.266667l164.693333 50.346666 54.613333 77.226667H170.666667c-94.293333 0-170.666667-76.373333-170.666667-170.666667V384.426667c0-94.293333 76.373333-170.666667 170.666667-170.666667h225.706666L341.76 290.986667 177.066667 341.333333z">
</path>
</svg>
)
}
Logo.defaultProps = {
className: '',
}
Logo.propTypes = {
className: PropTypes.string,
}
export default Logo

View File

@@ -0,0 +1,122 @@
import get from 'lodash.get'
import React, {useContext, useEffect, useRef, useState} from 'react'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {AppContext} from '../../reducer/App'
import {IconCaretDown, IconHelp} from '../icons'
import {Switch} from '../form'
const QuickLinks = () => {
const {theme, dispatch, language} = useContext(AppContext)
const [help, setHelp] = useState(false)
const wrapper = useRef(null)
const toggleTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark'
dispatch({
type: 'UPDATE',
payload: {
theme: newTheme,
},
})
}
const toggleLanguage = () => {
const newLanguage = language === 'en' ? 'zh' : 'en'
dispatch({
type: 'UPDATE',
payload: {
language: newLanguage,
},
})
}
useEffect(() => {
const clickOutside = event => {
const children = get(wrapper, 'current')
if (children && !children.contains(event.target)) {
setHelp(false)
}
}
document.addEventListener('mousedown', clickOutside)
return () => {
document.removeEventListener('mousedown', clickOutside)
}
}, [setHelp])
/*
<li>
<span className='switch-language'>
<Switch id='language-switch' isOn={language === 'en'} onChange={toggleLanguage} />
{language === 'en' ? 'English' : '中文'}
</span>
</li>
*/
return (
<ul className='quick-links'>
<li>
<span className='switch-mode'>
<Switch id='theme-switch' isOn={theme === 'dark'} onChange={toggleTheme}/>
{theme === 'dark' ? '深色' : '浅色'} 主题
</span>
</li>
<li>
<a
href='/'
className='dropdown'
tabIndex='-1'
onClick={e => {
e.preventDefault()
setHelp(!help)
}}
ref={wrapper}
>
<IconHelp/>
帮助信息
<IconCaretDown className='caret'/>
</a>
<TransitionGroup component={null}>
{help && (
<CSSTransition classNames='nav-anim' timeout={500}>
<ul className='dropdown-menu'>
<li>
<a
id='ql-help-projects'
target='_blank'
rel='noopener noreferrer'
href='https://spring.io/projects'
tabIndex='-1'
>
Spring Projects
</a>
</li>
<li>
<a
id='ql-help-guides'
target='_blank'
rel='noopener noreferrer'
tabIndex='-1'
href='https://spring.io/projects/spring-cloud/'
>
Spring Cloud
</a>
</li>
<li>
<a
id='ql-help-guides'
target='_blank'
rel='noopener noreferrer'
tabIndex='-1'
href='https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md'
>
Spring Cloud Alibaba
</a>
</li>
</ul>
</CSSTransition>
)}
</TransitionGroup>
</li>
</ul>
)
}
export default QuickLinks

View File

@@ -0,0 +1,3 @@
export { default as Layout } from './Layout'
export { default as Footer } from './Footer'
export { default as Header } from './Header'

View File

@@ -0,0 +1,27 @@
import '../../../styles/sandbox.scss'
import PropTypes from 'prop-types'
import React from 'react'
function SandboxShell({position}) {
return (
<button
className='button primary share-ghost'
type='button'
style={{
top: `${position.y}px`,
left: `${position.x}px`,
}}
>
跑一下 ^_^
</button>
)
}
SandboxShell.propTypes = {
position: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}).isRequired,
}
export default SandboxShell

View File

@@ -0,0 +1,35 @@
import PropTypes from 'prop-types'
import React from 'react'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {noScroll} from '../../utils/NoScroll'
function Overlay({ open }) {
const onEnter = () => {
noScroll.on()
}
const onEnded = () => {
noScroll.off()
}
return (
<TransitionGroup component={null}>
{open && (
<CSSTransition
onEnter={onEnter}
onExit={onEnded}
classNames='overlay'
timeout={500}
>
<div className='popup-share-overlay' />
</CSSTransition>
)}
</TransitionGroup>
)
}
Overlay.propTypes = {
open: PropTypes.bool.isRequired,
}
export default Overlay

View File

@@ -0,0 +1,146 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import React, {useEffect, useRef, useState} from 'react'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import useWindowsUtils from '../../utils/WindowsUtils'
import {IconTimes} from '../icons'
function Popover({ shareUrl, shareSrc, open, onClose, position }) {
const [button, setButton] = useState('Copy')
const wrapper = useRef(null)
const input = useRef(null)
const link = useRef(null)
const windowsUtils = useWindowsUtils()
useEffect(() => {
const clickOutside = event => {
const children = get(wrapper, 'current')
if (children && !children.contains(event.target)) {
onClose()
}
}
document.addEventListener('mousedown', clickOutside)
if (get(input, 'current')) {
get(input, 'current').focus()
}
return () => {
document.removeEventListener('mousedown', clickOutside)
}
}, [onClose, input])
const onEnter = () => {
setButton('复制')
}
const onCopy = () => {
setButton('已复制!')
input.current.focus()
setTimeout(() => {
onClose()
}, 500)
}
const urlToShare = `${windowsUtils.origin}/bootstrap.html/${shareSrc}#!${shareUrl}`
return (
<>
<TransitionGroup component={null}>
{open && (
<CSSTransition onEnter={onEnter} classNames='popup' timeout={500}>
<div
className='popup-share'
style={{
top: `${position.y - 200}px`,
left: `${position.x - 200}px`,
}}
>
<div ref={wrapper}>
<div className='popup-header'>
<h1>分享你的配置</h1>
<a
href='/#'
onClick={e => {
e.preventDefault()
onClose()
}}
className='close'
>
<IconTimes />
</a>
</div>
<div className='popup-content'>
{/* eslint-disable-next-line */}
<label htmlFor='input-share'>
请复制下面的链接来分享你的项目配置
</label>
{
shareSrc === ''
?
<label htmlFor='input-share'>
Tips: 您未登录<a href="https://account.aliyun.com/login/login.htm?oauth_callback=https%3A%2F%2Fstart.aliyun.com%2Fbootstrap.html">登录</a>
</label>
:
<label htmlFor='input-share'>
Tips: 您已登录您可以使用一下专属链接来分享项目配置该分享链接的点击将被关联在您的账户下
</label>
}
<div className='control'>
<input
onFocus={event => {
event.target.select()
}}
id='input-share'
className={`control-input ${
button === 'Copied!' ? 'padding-lg' : ''
}`}
readOnly
value={urlToShare}
ref={input}
/>
<CopyToClipboard onCopy={onCopy} text={urlToShare}>
<a
href='/#'
onClick={e => {
e.preventDefault()
}}
className='link'
ref={link}
>
{button}
</a>
</CopyToClipboard>
</div>
</div>
</div>
</div>
</CSSTransition>
)}
</TransitionGroup>
{open && (
<button
className='button primary share-ghost'
type='button'
style={{
top: `${position.y}px`,
left: `${position.x}px`,
}}
>
分享...
</button>
)}
</>
)
}
Popover.propTypes = {
shareUrl: PropTypes.string.isRequired,
shareSrc: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
position: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}).isRequired,
}
export default Popover

View File

@@ -0,0 +1,35 @@
import '../../../styles/share.scss'
import PropTypes from 'prop-types'
import React from 'react'
import Overlay from './Overlay'
import Popover from './Popover'
function Share({ shareUrl, shareSrc, open, onClose, position }) {
return (
<>
<Popover
open={open || false}
shareUrl={shareUrl}
shareSrc={shareSrc}
position={position}
onClose={onClose}
/>
<Overlay open={open || false} />
</>
)
}
Share.propTypes = {
shareUrl: PropTypes.string.isRequired,
shareSrc: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
position: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}).isRequired,
}
export default Share

View File

@@ -0,0 +1 @@
export { default as Share } from './Share'

View File

@@ -0,0 +1,134 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import set from 'lodash.set'
import React, {useReducer} from 'react'
import useTheme from '../utils/Theme'
import {isValidDependency} from '../utils/ApiUtils'
import {rangeToText} from '../utils/Version'
export const defaultAppContext = {
more: false,
complete: false,
explore: false,
share: false,
fetch: false,
tab: 'quicksearch',
theme: 'light',
language: 'zh',
config: {},
groupsOpened: [],
dependencies: {
list: [],
groups: [],
},
}
export function reduceDependencies(boot, items) {
const groups = []
const list = []
const getParent = (m, name) => {
return m.find(item => item.group === name)
}
for (let i = 0; i < items.length; i += 1) {
let message = ''
const dep = items[i]
let parent = getParent(groups, dep.group)
if (!parent) {
parent = {
group: dep.group,
items: [],
}
groups.push(parent)
}
const valid = isValidDependency(boot, dep)
if (!valid) {
message = `Requires Spring Boot ${rangeToText(
get(dep, 'versionRequirement')
)}.`
}
parent.items.push({ ...dep, valid, message })
list.push({ ...dep, valid, message })
}
return {
list,
groups,
}
}
export function reducer(state, action) {
switch (action.type) {
case 'UPDATE': {
const newState = { ...state }
const keysContext = Object.keys(defaultAppContext)
const keys = Object.keys(get(action, 'payload', {}))
keys.map(key => {
if (keysContext.indexOf(key) === -1) {
throw Error('Error AppProvider, invalid paylaod field action')
}
const value = get(action, `payload.${key}`)
set(newState, key, value)
if (key === 'theme') {
localStorage.setItem('springtheme', value)
}
return key
})
return newState
}
case 'TOGGLE_GROUP': {
const id = get(action, 'payload.id')
let groupsOpened = [...state.groupsOpened]
if (groupsOpened.indexOf(id) > -1) {
groupsOpened = [...groupsOpened.filter(g => g !== id)]
} else {
groupsOpened = [...groupsOpened, id]
}
return { ...state, groupsOpened }
}
case 'UPDATE_DEPENDENCIES': {
const dependencies = reduceDependencies(
get(action, 'payload.boot'),
get(state, 'config.lists.dependencies')
)
return { ...state, dependencies }
}
case 'SHARE_UPDATE': {
const share = get(action, 'payload.open', false)
return { ...state, share }
}
case 'EXPLORE_UPDATE': {
const explore = get(action, 'payload.open', false)
return { ...state, explore }
}
case 'FETCH_UPDATE': {
const fetch = get(action, 'payload.open', false)
return { ...state, fetch }
}
case 'COMPLETE': {
const json = get(action, 'payload', {})
const dependencies = reduceDependencies(
get(json, 'defaultValues.boot'),
get(json, 'lists.dependencies')
)
return { ...state, complete: true, config: json, dependencies }
}
default:
return state
}
}
export const AppContext = React.createContext({ ...defaultAppContext })
export function AppProvider({ children }) {
const theme = useTheme()
const [state, dispatch] = useReducer(reducer, { ...defaultAppContext, theme })
return (
<AppContext.Provider value={{ ...state, dispatch }}>
{children}
</AppContext.Provider>
)
}
AppProvider.propTypes = {
children: PropTypes.node.isRequired,
}

View File

@@ -0,0 +1,132 @@
import PropTypes from 'prop-types'
import get from 'lodash.get'
import set from 'lodash.set'
import React, {useReducer} from 'react'
import {getShareUrl, parseParams} from '../utils/ApiUtils'
export const defaultInitializrContext = {
values: {
project: '',
language: '',
architecture: '',
boot: '',
meta: {
name: '',
group: '',
artifact: '',
description: '',
packaging: '',
packageName: '',
java: '',
},
dependencies: [],
share: [],
},
share: '',
errors: {},
warnings: {},
}
export function reducer(state, action) {
switch (action.type) {
case 'COMPLETE': {
const json = get(action, 'payload')
const defaultValues = {
...get(json, 'defaultValues'),
meta: get(json, 'defaultValues.meta'),
}
return {
values: defaultValues,
share: getShareUrl(defaultValues),
errors: {},
warnings: {},
}
}
case 'UPDATE': {
const changes = get(action, 'payload')
let errors = { ...state.errors }
let meta = { ...get(state, 'values.meta') }
if (get(changes, 'meta')) {
meta = { ...meta, ...get(changes, 'meta') }
}
if (get(changes, 'boot')) {
const { boot, ...err } = errors
errors = err
}
if (get(changes, 'meta.group')) {
set(
meta,
'packageName',
`${get(meta, 'group')}.${get(meta, 'artifact')}`
)
}
if (get(changes, 'meta.artifact')) {
set(
meta,
'packageName',
`${get(meta, 'group')}.${get(meta, 'artifact')}`
)
set(meta, 'name', `${get(meta, 'artifact')}`)
}
const values = {
...get(state, 'values'),
...changes,
meta,
}
return { ...state, values, share: getShareUrl(values), errors }
}
case 'LOAD': {
const params = get(action, 'payload.params')
const lists = get(action, 'payload.lists')
const { values, errors, warnings } = parseParams(
state.values,
params,
lists
)
return { ...state, values, errors, warnings, share: getShareUrl(values) }
}
case 'ADD_DEPENDENCY': {
const dependency = get(action, 'payload.id')
const values = { ...get(state, 'values') }
if(get(values, 'dependencies', []).filter(item => item === dependency).length == 0){
values.dependencies = [...get(values, 'dependencies'), dependency]
}
return { ...state, values, share: getShareUrl(values) }
}
case 'REMOVE_DEPENDENCY': {
const dependency = get(action, 'payload.id')
const values = { ...get(state, 'values') }
values.dependencies = [
...get(values, 'dependencies').filter(dep => dep !== dependency),
]
return { ...state, values, share: getShareUrl(values) }
}
case 'CLEAR_WARNINGS': {
return { ...state, warnings: {} }
}
default:
return state
}
}
export const InitializrContext = React.createContext({
...defaultInitializrContext,
})
export function InitializrProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { ...defaultInitializrContext })
return (
<InitializrContext.Provider value={{ ...state, dispatch }}>
{children}
</InitializrContext.Provider>
)
}
InitializrProvider.defaultProps = {
children: null,
}
InitializrProvider.propTypes = {
children: PropTypes.node,
}

View File

@@ -0,0 +1,137 @@
import get from 'lodash.get'
import {reducer} from '../App'
let state = {}
beforeEach(() => {
state = {
more: false,
complete: false,
explore: false,
share: false,
tab: 'quicksearch',
theme: 'light',
config: {},
groupsOpened: [],
dependencies: {
list: [],
groups: [],
},
}
})
describe('UPDATE action', () => {
it('should reduce the state', () => {
let result = reducer(state, { type: 'UPDATE', payload: { more: true } })
expect(get(result, 'more')).toBe(true)
result = reducer(state, { type: 'UPDATE', payload: { theme: 'dark' } })
expect(get(result, 'theme')).toBe('dark')
})
it('should throw an error', () => {
expect(() => {
reducer(state, { type: 'UPDATE', payload: { foo: 'bar' } })
}).toThrow(Error)
})
})
describe('TOGGLE_GROUP action', () => {
it('should add/remove a group', () => {
let result = reducer(state, {
type: 'TOGGLE_GROUP',
payload: { id: 'foo1' },
})
expect(get(result, 'groupsOpened').length).toBe(1)
expect(get(result, 'groupsOpened[0]')).toBe('foo1')
result = reducer(result, {
type: 'TOGGLE_GROUP',
payload: { id: 'foo2' },
})
expect(get(result, 'groupsOpened').length).toBe(2)
expect(get(result, 'groupsOpened[0]')).toBe('foo1')
expect(get(result, 'groupsOpened[1]')).toBe('foo2')
result = reducer(result, {
type: 'TOGGLE_GROUP',
payload: { id: 'foo1' },
})
expect(get(result, 'groupsOpened').length).toBe(1)
expect(get(result, 'groupsOpened[0]')).toBe('foo2')
})
})
describe('UPDATE_DEPENDENCIES action', () => {
it('should update dependencies', () => {
state.config = {
lists: {
dependencies: [
{
id: 'foo1',
group: 'bar2',
versionRange: '',
},
],
},
}
const result = reducer(state, {
type: 'UPDATE_DEPENDENCIES',
payload: {
boot: '1.0.0.RELEASE',
},
})
expect(get(result, 'dependencies.list').length).toBe(1)
expect(get(result, 'dependencies.list[0].id')).toBe('foo1')
expect(get(result, 'dependencies.list[0].valid')).toBe(true)
})
})
describe('SHARE_UPDATE/EXPLORE_UPDATE action', () => {
it('should add/remove a group', () => {
let result = reducer(state, {
type: 'SHARE_UPDATE',
payload: { open: true },
})
expect(get(result, 'share')).toBe(true)
result = reducer(result, {
type: 'SHARE_UPDATE',
payload: { open: false },
})
expect(get(result, 'share')).toBe(false)
result = reducer(result, {
type: 'EXPLORE_UPDATE',
payload: { open: true },
})
expect(get(result, 'explore')).toBe(true)
result = reducer(result, {
type: 'EXPLORE_UPDATE',
payload: { open: false },
})
expect(get(result, 'explore')).toBe(false)
})
})
describe('COMPLETE action', () => {
it('should add/remove a group', () => {
const result = reducer(state, {
type: 'COMPLETE',
payload: {
defaultValues: {
boot: '1.0.0',
},
lists: {
dependencies: [
{
id: 'foo1',
group: 'bar2',
versionRange: '',
},
],
},
},
})
expect(get(result, 'complete')).toBe(true)
expect(get(result, 'config.json')).not.toBe(null)
expect(get(result, 'dependencies')).not.toBe(null)
expect(get(result, 'dependencies.list').length).toBe(1)
})
})

View File

@@ -0,0 +1,289 @@
import get from 'lodash.get'
import MockClient from '../../../../dev/api.json'
import {getDefaultValues, getLists} from '../../utils/ApiUtils'
import {reducer} from '../Initializr'
const defaultValues = getDefaultValues({ ...MockClient })
let state = {}
beforeEach(() => {
state = {
values: { ...defaultValues, meta: { ...defaultValues.meta } },
share: '',
errors: {},
warnings: {},
}
})
describe('COMPLETE action', () => {
beforeEach(() => {
state = {
values: {
project: '',
language: '',
architecture: '',
boot: '',
meta: {
name: '',
group: '',
artifact: '',
description: '',
packaging: '',
packageName: '',
java: '',
},
dependencies: [],
},
share: '',
errors: {},
warnings: {},
}
})
it('should reduce the state', () => {
const result = reducer(state, {
type: 'COMPLETE',
payload: {
defaultValues,
},
})
expect(get(result, 'share')).toBe(
'type=maven-project&language=java&architecture=none&platformVersion=2.2.0.RELEASE&packaging=jar&jvmVersion=1.8&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo'
)
expect(get(result, 'values.project')).toBe('maven-project')
expect(get(result, 'values.language')).toBe('java')
expect(get(result, 'values.architecture')).toBe('none')
expect(get(result, 'values.boot')).toBe('2.2.0.RELEASE')
expect(get(result, 'values.meta.name')).toBe('demo')
expect(get(result, 'values.meta.group')).toBe('com.example')
expect(get(result, 'values.meta.artifact')).toBe('demo')
expect(get(result, 'values.meta.description')).toBe(
'Demo project for Spring Boot'
)
expect(get(result, 'values.meta.packaging')).toBe('jar')
expect(get(result, 'values.meta.packageName')).toBe('com.example.demo')
expect(get(result, 'values.meta.java')).toBe('1.8')
expect(get(result, 'values.dependencies').length).toBe(0)
expect(Object.keys(get(result, 'errors')).length).toBe(0)
expect(Object.keys(get(result, 'warnings')).length).toBe(0)
})
})
describe('UPDATE action', () => {
it('should reduce the state (type)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
project: 'maven-project',
},
})
expect(get(result, 'values.project')).toBe('maven-project')
})
it('should reduce the state (language)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
language: 'kotlin',
},
})
expect(get(result, 'values.language')).toBe('kotlin')
})
it('should reduce the state (boot)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
boot: '2.1.10.BUILD-SNAPSHOT',
},
})
expect(get(result, 'values.boot')).toBe('2.1.10.BUILD-SNAPSHOT')
})
it('should reduce the state (meta name)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
name: 'demo1',
},
},
})
expect(get(result, 'values.meta.name')).toBe('demo1')
})
it('should reduce the state (meta group)', () => {
state.values.meta.artifact = 'demo3'
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
group: 'com.example1',
},
},
})
expect(get(result, 'values.meta.group')).toBe('com.example1')
expect(get(result, 'values.meta.packageName')).toBe('com.example1.demo3')
})
it('should reduce the state (meta artifact)', () => {
state.values.meta.group = 'com.example3'
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
artifact: 'demo2',
},
},
})
expect(get(result, 'values.meta.artifact')).toBe('demo2')
expect(get(result, 'values.meta.packageName')).toBe('com.example3.demo2')
})
it('should reduce the state (meta description)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
description: 'desc',
},
},
})
expect(get(result, 'values.meta.description')).toBe('desc')
})
it('should reduce the state (meta packaging)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
packaging: 'war',
},
},
})
expect(get(result, 'values.meta.packaging')).toBe('war')
})
it('should reduce the state (meta packageName)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
packageName: 'com1.example2.demo3',
},
},
})
expect(get(result, 'values.meta.packageName')).toBe('com1.example2.demo3')
})
it('should reduce the state (meta java)', () => {
const result = reducer(state, {
type: 'UPDATE',
payload: {
meta: {
java: '1.9',
},
},
})
expect(get(result, 'values.meta.java')).toBe('1.9')
})
})
describe('LOAD action', () => {
it('should reduce the state', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const result = reducer(state, {
type: 'LOAD',
payload: {
params: {
artifactId: 'demo1',
description: 'Demo1 project for Spring Boot',
groupId: 'com.example1',
jvmVersion: '1.8',
language: 'java',
architecture: 'none',
name: 'demo1',
packageName: 'com.example1.demo1',
packaging: 'war',
platformVersion: '2.2.0.RELEASE',
type: 'gradle-project',
},
lists: listsValues,
},
})
expect(get(result, 'values.project')).toBe('gradle-project')
expect(get(result, 'values.language')).toBe('java')
expect(get(result, 'values.boot')).toBe('2.2.0.RELEASE')
expect(get(result, 'values.meta.name')).toBe('demo1')
expect(get(result, 'values.meta.group')).toBe('com.example1')
expect(get(result, 'values.meta.artifact')).toBe('demo1')
expect(get(result, 'values.meta.description')).toBe(
'Demo1 project for Spring Boot'
)
expect(get(result, 'values.meta.packaging')).toBe('war')
expect(get(result, 'values.meta.packageName')).toBe('com.example1.demo1')
expect(get(result, 'values.meta.java')).toBe('1.8')
expect(get(result, 'values.dependencies').length).toBe(0)
expect(Object.keys(get(result, 'errors')).length).toBe(0)
expect(Object.keys(get(result, 'warnings')).length).toBe(0)
})
})
describe('ADD_DEPENDENCY action', () => {
it('should reduce the state', () => {
let result = reducer(state, {
type: 'ADD_DEPENDENCY',
payload: {
id: 'foo1',
},
})
expect(get(result, 'values.dependencies').length).toBe(1)
expect(get(result, 'values.dependencies[0]')).toBe('foo1')
result = reducer(result, {
type: 'ADD_DEPENDENCY',
payload: {
id: 'foo2',
},
})
expect(get(result, 'values.dependencies').length).toBe(2)
expect(get(result, 'values.dependencies[0]')).toBe('foo1')
expect(get(result, 'values.dependencies[1]')).toBe('foo2')
})
})
describe('REMOVE_DEPENDENCY action', () => {
it('should reduce the state', () => {
let result = reducer(state, {
type: 'ADD_DEPENDENCY',
payload: {
id: 'foo1',
},
})
result = reducer(result, {
type: 'ADD_DEPENDENCY',
payload: {
id: 'foo2',
},
})
expect(get(result, 'values.dependencies').length).toBe(2)
expect(get(result, 'values.dependencies[0]')).toBe('foo1')
expect(get(result, 'values.dependencies[1]')).toBe('foo2')
result = reducer(result, {
type: 'REMOVE_DEPENDENCY',
payload: {
id: 'foo1',
},
})
expect(get(result, 'values.dependencies').length).toBe(1)
expect(get(result, 'values.dependencies[0]')).toBe('foo2')
result = reducer(result, {
type: 'REMOVE_DEPENDENCY',
payload: {
id: 'foo2',
},
})
expect(get(result, 'values.dependencies').length).toBe(0)
})
})
describe('CLEAR_WARNINGS action', () => {
it('should reduce the state', () => {
state.warnings = { foo: 'bar' }
const result = reducer(state, {
type: 'CLEAR_WARNINGS',
})
expect(Object.keys(get(result, 'warnings')).length).toBe(0)
})
})

View File

@@ -0,0 +1,326 @@
import get from 'lodash.get'
import querystring from 'querystring'
import set from 'lodash.set'
import Extend from '../../Extend.json'
import {isInRange, parseReleases, parseVersion} from './Version'
const PROPERTIES_MAPPING_URL = {
type: 'project',
language: 'language',
architecture: 'architecture',
platformVersion: 'boot',
packaging: 'meta.packaging',
jvmVersion: 'meta.java',
groupId: 'meta.group',
artifactId: 'meta.artifact',
name: 'meta.name',
description: 'meta.description',
packageName: 'meta.packageName',
dependencies: 'dependencies',
}
export const getInfo = function getInfo(url) {
return new Promise((resolve, reject) => {
fetch(`${url}`, {
method: 'GET',
headers: {
Accept: 'application/vnd.initializr.v2.1+json',
},
})
.then(
response => response.json(),
() => {
reject()
return null
}
)
.then(data => {
if (data) {
resolve(data)
}
})
})
}
export const getShareUrl = values => {
const props = {}
Object.keys(PROPERTIES_MAPPING_URL).forEach(key => {
const key2 = get(PROPERTIES_MAPPING_URL, key)
const value = get(values, key2)
if (key !== 'dependencies') {
set(props, key, value)
}
})
let params = `${querystring.stringify(props)}`
if (get(values, 'dependencies', []).length > 0) {
params = `${params}&dependencies=${get(values, 'dependencies').join(',')}`
}
return params
}
export const isValidParams = params => {
return (
Object.keys(params)
.map(entry => {
return !!get(PROPERTIES_MAPPING_URL, entry, null)
})
.filter(item => !!item).length > 0
)
}
export const parseParams = (values, queryParams, lists) => {
const errors = {}
const warnings = {}
if (isValidParams(queryParams)) {
Object.keys(queryParams).forEach(entry => {
const key = get(PROPERTIES_MAPPING_URL, entry)
if (key) {
const value = get(queryParams, entry, '').toLowerCase()
switch (key) {
case 'project':
case 'language':
case 'architecture':
case 'meta.packaging':
case 'meta.java': {
const list = get(lists, key, [])
const res = list.find(a => a.key.toLowerCase() === value)
if (res) {
set(values, key, res.key)
} else {
const currentValue = list.find(
a => a.key.toLowerCase() === get(values, key)
)
set(warnings, key, {
value: get(queryParams, entry, ''),
select: currentValue.text,
})
}
break
}
case 'boot': {
const list = get(lists, key, [])
const res = list.find(a => a.key.toLowerCase() === value)
let error = false
if (res) {
set(values, key, res.key)
} else {
error = true
let versionMajor = value
if (versionMajor.indexOf('.x') === -1) {
versionMajor = get(parseVersion(versionMajor), 'major', '')
}
if (versionMajor.indexOf('.x') > -1) {
const releases = parseReleases(list).filter(
release =>
release.major.toLowerCase() === versionMajor.toLowerCase()
)
if (releases.length > 0) {
const release = releases.reduce((p, c) => {
if (p.qualify > c.qualify) {
return p
}
if (p.qualify === c.qualify) {
if (p.minor > c.minor) {
return p
}
}
return c
}, releases[0])
if (release) {
error = false
set(values, key, release.version)
const currentValue = list.find(
a => a.key.toLowerCase() === release.version.toLowerCase()
)
set(warnings, key, {
value: get(queryParams, entry, ''),
select: currentValue.text,
})
}
}
}
}
if (error) {
set(errors, 'boot', {
value: get(queryParams, entry, ''),
})
}
break
}
case 'dependencies': {
const depsWarning = []
const newVal = value
.split(',')
.map(item => {
const dep = get(lists, 'dependencies').find(
d => d.id === item.trim()
)
if (dep) {
return dep.id
}
depsWarning.push(item)
return null
})
.filter(item => !!item)
if (depsWarning.length > 0) {
set(warnings, key, {
value: depsWarning.join(', '),
})
}
set(values, key, newVal)
break
}
default:
set(values, key, get(queryParams, entry, ''))
}
}
})
}
return {
values,
errors,
warnings,
}
}
export const getLists = json => {
const deps = []
get(json, 'dependencies.values', []).forEach(group => {
group.values.forEach(item => {
const extend = Extend.find(it => it.id === get(item, 'id', ''))
const val = {
id: `${get(item, 'id', '')}`,
name: `${get(item, 'name', '')}`,
group: `${group.name}`,
description: `${get(item, 'description', '')}`,
versionRange: `${get(item, 'versionRange', '')}`,
versionRequirement: `${get(item, 'versionRange', '')}`,
weight: get(extend, 'weight', 50),
}
deps.push(val)
})
})
return {
project: get(json, 'type.values', [])
.filter(type => type.action === '/starter.zip')
.map(type => ({
key: `${type.id}`,
text: `${type.name}`,
})),
language: get(json, 'language.values', []).map(language => ({
key: `${language.id}`,
text: `${language.name}`,
})),
architecture: get(json, 'architecture.values', []).map(architecture => ({
key: `${architecture.id}`,
text: `${architecture.name}`,
dependencies: get(architecture, 'dependOn', []),
})),
boot: get(json, 'bootVersion.values', []).map(boot => ({
key: `${boot.id}`,
text: `${boot.name}`,
})),
meta: {
java: get(json, 'javaVersion.values', []).map(java => ({
key: `${java.id}`,
text: `${java.name}`,
})),
packaging: get(json, 'packaging.values', []).map(packaging => ({
key: `${packaging.id}`,
text: `${packaging.name}`,
})),
},
dependencies: deps,
}
}
export const getDefaultValues = json => {
return {
project: get(json, 'type.default'),
language: get(json, 'language.default'),
architecture: get(json, 'architecture.default'),
boot: get(json, 'bootVersion.default'),
meta: {
name: get(json, 'name.default'),
group: get(json, 'groupId.default'),
artifact: get(json, 'artifactId.default'),
description: get(json, 'description.default'),
packaging: get(json, 'packaging.default'),
packageName: get(json, 'packageName.default'),
java: get(json, 'javaVersion.default'),
},
share: get(json, 'share.default'),
dependencies: [],
availableCount: [],
}
}
export const getConfig = json => {
return {
lists: getLists(json),
defaultValues: getDefaultValues(json),
}
}
export const isValidDependency = function isValidDependency(boot, dependency) {
if (!dependency) {
return false
}
return get(dependency, 'versionRange')
? isInRange(boot, get(dependency, 'versionRange'))
: true
}
export const getQueryString = function getQueryString(values, config) {
const params = querystring.stringify({
type: get(values, 'project'),
language: get(values, 'language'),
architecture: get(values, 'architecture'),
bootVersion: get(values, 'boot'),
baseDir: get(values, 'meta.artifact'),
groupId: get(values, 'meta.group'),
artifactId: get(values, 'meta.artifact'),
name: get(values, 'meta.name'),
description: get(values, 'meta.description'),
packageName: get(values, 'meta.packageName'),
packaging: get(values, 'meta.packaging'),
javaVersion: get(values, 'meta.java'),
})
let paramsDependencies = get(values, 'dependencies', [])
.map(dependency => {
const dep = config.find(it => it.id === dependency)
return isValidDependency(get(values, 'boot'), dep) ? dependency : null
})
.filter(dep => !!dep)
.join(',')
if (paramsDependencies) {
paramsDependencies = `&dependencies=${paramsDependencies}`
}
return `${params}${paramsDependencies}`
}
export const getProject = function getProject(url, values, config) {
return new Promise((resolve, reject) => {
const paramsStr = getQueryString(values, config)
fetch(`${url}?${paramsStr}`, {
method: 'GET',
}).then(
response => {
if (response.status === 200) {
resolve(response.blob())
return
}
reject()
},
() => {
reject()
}
)
})
}

View File

@@ -0,0 +1,52 @@
import queryString from 'query-string'
import {toast} from 'react-toastify'
import {useContext, useEffect, useState} from 'react'
import {AppContext} from '../reducer/App'
import {InitializrContext} from '../reducer/Initializr'
import {isValidParams} from './ApiUtils'
const getHash = () => {
return window.location.hash
}
const clearHash = () => {
if (window.location.hash) {
if (window.history.pushState) {
window.history.pushState(null, null, window.location.pathname)
} else {
window.history.hash = ``
}
}
}
export default function useHash() {
const [hash, setHash] = useState(getHash())
const { dispatch } = useContext(InitializrContext)
const { config, complete } = useContext(AppContext)
useEffect(() => {
const handler = () => {
setHash(getHash())
}
window.addEventListener('hashchange', handler)
return () => {
window.removeEventListener('hashchange', handler)
}
}, [])
useEffect(() => {
if (complete && hash) {
const params = queryString.parse(`?${hash.substr(2)}`)
dispatch({ type: 'LOAD', payload: { params, lists: config.lists } })
clearHash()
setHash('')
if (isValidParams(params)) {
toast.success(`分享配置 已加载.`)
}
}
}, [complete, hash, dispatch, config])
return null
}

View File

@@ -0,0 +1,8 @@
export const noScroll = {
on: () => {
document.documentElement.style.overflow = 'hidden'
},
off: () => {
document.documentElement.style.overflow = ''
},
}

View File

@@ -0,0 +1,18 @@
import {useState} from 'react'
function getTheme() {
const isDarkConfig =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
const theme = localStorage.getItem('springtheme')
if (!theme) {
return isDarkConfig ? 'dark' : 'light'
}
return theme
}
export default function useTheme() {
const [darkTheme] = useState(getTheme())
return darkTheme
}

View File

@@ -0,0 +1,115 @@
const strictRange = /\[(.*),(.*)\]/
const halfopenRightRange = /\[(.*),(.*)\)/
const halfopenLeftRange = /\((.*),(.*)\]/
const qualifiers = ['M', 'RC', 'BUILD-SNAPSHOT', 'RELEASE']
export const parseQualifier = version => {
const qual = (version || '')
.replace(/\d+/g, '')
.replace(/\./g, ' ')
.replace(/\s/g, '')
return qualifiers.indexOf(qual) > -1 ? qual : 'RELEASE'
}
export const parseVersion = version => {
const r = version.toString().split('.')
if (r.length < 2) {
return {
version,
}
}
return {
version,
short: `${r[0]}.${r[1]}.${r[2]}`,
major: `${r[0]}.${r[1]}.x`,
qualify: qualifiers.indexOf(parseQualifier(version)),
minor: +r[2],
}
}
export const compare = (a, b) => {
let result
const versionA = a.split('.')
const versionB = b.split('.')
if (versionA.length === 3) {
versionA[3] = ''
}
if (versionB.length === 3) {
versionB[3] = ''
}
for (let i = 0; i < 3; i += 1) {
result = parseInt(versionA[i], 10) - parseInt(versionB[i], 10)
if (result !== 0) {
return result
}
}
const qualify = version => qualifiers.indexOf(parseQualifier(version))
result = qualify(a) - qualify(b)
if (result !== 0) {
return result
}
return versionA[3].localeCompare(versionB[3])
}
export const parseReleases = releases => {
return releases.map(release => {
const version = parseVersion(release.key)
return version
})
}
export const isInRange = (version, range) => {
if (!range) {
return true
}
const strickMatch = range.match(strictRange)
if (strickMatch) {
return (
compare(strickMatch[1], version) <= 0 &&
compare(strickMatch[2], version) >= 0
)
}
const horMatch = range.match(halfopenRightRange)
if (horMatch) {
return (
compare(horMatch[1], version) <= 0 && compare(horMatch[2], version) > 0
)
}
const holMatch = range.match(halfopenLeftRange)
if (holMatch) {
return (
compare(holMatch[1], version) < 0 && compare(holMatch[2], version) >= 0
)
}
return compare(range, version) <= 0
}
export const rangeToText = range => {
const strictMatch = range.match(strictRange)
if (strictMatch) {
return `>= ${strictMatch[1]} and <= ${strictMatch[2]}`
}
const horMatch = range.match(halfopenRightRange)
if (horMatch) {
return `>= ${horMatch[1]} and < ${horMatch[2]}`
}
const holMatch = range.match(halfopenLeftRange)
if (holMatch) {
return `> ${holMatch[1]} and <= ${holMatch[2]}`
}
return `>= ${range}`
}
export const getValidDependencies = (boot, dependencies) => {
return dependencies
.map(dep => {
const compatibility = dep.versionRange
? isInRange(boot, dep.versionRange)
: true
if (!compatibility) {
return null
}
return dep
})
.filter(d => !!d)
}

View File

@@ -0,0 +1,19 @@
import {useState} from 'react'
function getProperties() {
return {
symb:
window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
? '⌘'
: 'Ctrl',
origin: window.location.origin,
pathname: window.location.pathname,
}
}
export default function useWindowsUtils() {
const [symb] = useState(getProperties().symb)
const [origin] = useState(getProperties().origin)
const [pathname] = useState(getProperties().pathname)
return { symb, origin, pathname }
}

View File

@@ -0,0 +1,90 @@
import get from 'lodash.get'
const FILE_EXTENSION = {
js: 'javascript',
md: 'markdown',
kt: 'kotlin',
kts: 'kotlin',
gradle: 'groovy',
gitignore: 'git',
java: 'java',
xml: 'xml',
properties: 'properties',
groovy: 'groovy',
html: 'html',
css: 'css',
}
export const getLanguage = file => {
if (!file.includes(`.`)) {
return null
}
const extension = file.split(`.`).pop()
return get(FILE_EXTENSION, extension, null)
}
export const createTree = (files, path, fileName, zip) => {
return new Promise(resolve => {
const recursive = (pfiles, ppath, pfileName, pzip, pdepth) => {
const type = pfiles[ppath].dir ? 'folder' : 'file'
const item = {
type,
filename: pfileName,
path: `/${ppath}`,
hidden: pdepth === 1 && type === 'folder' ? true : null,
}
if (type === 'folder') {
const children = []
pzip.folder(ppath).forEach((relativePath, file) => {
const pathArray = relativePath.split('/')
if (pathArray.length === 1 || (file.dir && pathArray.length === 2)) {
children.push(
recursive(
pfiles,
ppath + relativePath,
relativePath,
pzip,
pdepth + 1
)
)
}
})
item.children = children.sort((a, b) => (a.path > b.path ? 1 : -1))
item.filename = pfileName.substring(0, pfileName.length - 1)
} else {
item.language = getLanguage(item.filename)
if (item.language) {
pfiles[ppath].async('string').then(content => {
item.content = content
})
}
}
return item
}
const tree = recursive(files, path, fileName, zip, 0)
const selected = tree.children.find(
item =>
['pom.xml', 'build.gradle', 'build.gradle.kts','settings.gradle','settings.gradle.kts',].indexOf(item.filename) >
-1
)
if (selected) {
files[selected.path.substring(1)].async('string').then(content => {
selected.content = content
resolve({ tree, selected })
})
} else {
resolve({ tree, selected: null })
}
})
}
export const findRoot = zip => {
const root = Object.keys(zip.files).filter(filename => {
const pathArray = filename.split('/')
if (zip.files[filename].dir && pathArray.length === 2) {
return true
}
return false
})[0]
return root.substring(0, root.length - 1)
}

View File

@@ -0,0 +1,423 @@
import get from 'lodash.get'
import MockClient from '../../../../dev/api.json'
import {getDefaultValues, getLists, getProject, getShareUrl, parseParams,} from '../ApiUtils'
/**
* Function getDefaultValues
*/
describe('getDefaultValues', () => {
it('parse correctly', () => {
const json = { ...MockClient }
const defaultValues = getDefaultValues(json)
expect(get(defaultValues, 'project')).toBe(get(MockClient, 'type.default'))
expect(get(defaultValues, 'boot')).toBe(
get(MockClient, 'bootVersion.default')
)
expect(get(defaultValues, 'language')).toBe(
get(MockClient, 'language.default')
)
expect(get(defaultValues, 'architecture')).toBe(
get(MockClient, 'architecture.default')
)
expect(get(defaultValues, 'meta.name')).toBe(
get(MockClient, 'name.default')
)
expect(get(defaultValues, 'meta.group')).toBe(
get(MockClient, 'groupId.default')
)
expect(get(defaultValues, 'meta.artifact')).toBe(
get(MockClient, 'artifactId.default')
)
expect(get(defaultValues, 'meta.description')).toBe(
get(MockClient, 'description.default')
)
expect(get(defaultValues, 'meta.packaging')).toBe(
get(MockClient, 'packaging.default')
)
expect(get(defaultValues, 'meta.packageName')).toBe(
get(MockClient, 'packageName.default')
)
expect(get(defaultValues, 'meta.java')).toBe(
get(MockClient, 'javaVersion.default')
)
})
})
/**
* Function getListValues
*/
describe('getListValues', () => {
it('parse correctly the list of projects', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listProjects = get(listsValues, 'project')
const mockProjects = get(MockClient, 'type.values').filter(
project => project.action === '/starter.zip'
)
expect(listProjects.length).toBe(mockProjects.length)
for (let i = 0; i < mockProjects.length; i += 1) {
expect(listProjects[i].key).toBe(mockProjects[i].id)
expect(listProjects[i].text).toBe(mockProjects[i].name)
}
})
it('parse correctly the list of languages', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listLanguages = get(listsValues, 'language')
const mockLanguages = get(MockClient, 'language.values')
expect(listLanguages.length).toBe(mockLanguages.length)
for (let i = 0; i < mockLanguages.length; i += 1) {
expect(listLanguages[i].key).toBe(mockLanguages[i].id)
expect(listLanguages[i].text).toBe(mockLanguages[i].name)
}
})
it('parse correctly the list of boot versions', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listBootVersions = get(listsValues, 'boot')
const mockBootVersions = get(MockClient, 'bootVersion.values')
expect(listBootVersions.length).toBe(mockBootVersions.length)
for (let i = 0; i < mockBootVersions.length; i += 1) {
expect(listBootVersions[i].key).toBe(mockBootVersions[i].id)
expect(listBootVersions[i].text).toBe(mockBootVersions[i].name)
}
})
it('parse correctly the list of java versions', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listJavaVersions = get(listsValues, 'meta.java')
const mockJavaVersions = get(MockClient, 'javaVersion.values')
expect(listJavaVersions.length).toBe(mockJavaVersions.length)
for (let i = 0; i < mockJavaVersions.length; i += 1) {
expect(listJavaVersions[i].key).toBe(mockJavaVersions[i].id)
expect(listJavaVersions[i].text).toBe(mockJavaVersions[i].name)
}
})
it('parse correctly the list of packagings', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listPackagings = get(listsValues, 'meta.packaging')
const mockPackagings = get(MockClient, 'packaging.values')
expect(listPackagings.length).toBe(mockPackagings.length)
for (let i = 0; i < mockPackagings.length; i += 1) {
expect(listPackagings[i].key).toBe(mockPackagings[i].id)
expect(listPackagings[i].text).toBe(mockPackagings[i].name)
}
})
it('parse correctly the list of dependencies', () => {
const json = { ...MockClient }
const listsValues = getLists(json)
const listDependencies = get(listsValues, 'dependencies')
const mockDependencies = get(MockClient, 'dependencies.values').reduce(
(p, c) => {
p.push(...get(c, 'values'))
return p
},
[]
)
expect(listDependencies.length).toBe(mockDependencies.length)
for (let i = 0; i < mockDependencies.length; i += 1) {
expect(listDependencies[i].id).toBe(mockDependencies[i].id)
expect(listDependencies[i].name).toBe(mockDependencies[i].name)
expect(listDependencies[i].description).toBe(
mockDependencies[i].description
)
}
})
})
/**
* Function parseParams
*/
describe('parseParams', () => {
it('return parameters, no warning and no error', () => {
const json = { ...MockClient }
const defaultValues = getDefaultValues(json)
const listsValues = getLists(json)
const result = parseParams(
defaultValues,
{
type: 'gradle-project',
language: 'kotlin',
architecture: 'none',
platformVersion: get(defaultValues, 'boot'),
packaging: 'war',
jvmVersion: '11',
groupId: 'com.example2',
artifactId: 'demo2',
name: 'demo2',
description: 'Demo project for Spring Boot2',
packageName: 'com.example2.demo2',
dependencies: ' devtools , lombok ',
},
listsValues
)
expect(Object.keys(result.warnings).length).toBe(0)
expect(Object.keys(result.errors).length).toBe(0)
expect(get(result, 'values.project')).toBe('gradle-project')
expect(get(result, 'values.language')).toBe('kotlin')
expect(get(result, 'values.boot')).toBe(get(defaultValues, 'boot'))
expect(get(result, 'values.meta.packaging')).toBe('war')
expect(get(result, 'values.meta.java')).toBe('11')
expect(get(result, 'values.meta.group')).toBe('com.example2')
expect(get(result, 'values.meta.artifact')).toBe('demo2')
expect(get(result, 'values.meta.name')).toBe('demo2')
expect(get(result, 'values.meta.description')).toBe(
'Demo project for Spring Boot2'
)
expect(get(result, 'values.meta.packageName')).toBe('com.example2.demo2')
expect(get(result, 'values.dependencies').length).toBe(2)
})
it('return parameters, warnings and no error', () => {
const json = { ...MockClient }
const defaultValues = getDefaultValues(json)
const listsValues = getLists(json)
const result = parseParams(
defaultValues,
{
type: 'ant-project',
language: 'php',
architecture: 'none',
platformVersion: get(defaultValues, 'boot'),
packaging: 'tar',
jvmVersion: '1',
groupId: 'com.example',
artifactId: 'demo',
name: 'demo',
description: 'Demo project for Spring Boot',
packageName: 'com.example.demo',
dependencies: 'a,devtools,b,lombok',
},
listsValues
)
expect(get(result, 'warnings.project.value')).toBe('ant-project')
expect(get(result, 'warnings.language.value')).toBe('php')
expect(get(result, 'warnings.meta.packaging.value')).toBe('tar')
expect(get(result, 'warnings.meta.java.value')).toBe('1')
expect(get(result, 'values.meta.group')).toBe('com.example')
expect(get(result, 'values.meta.artifact')).toBe('demo')
expect(get(result, 'values.meta.name')).toBe('demo')
expect(get(result, 'values.meta.description')).toBe(
'Demo project for Spring Boot'
)
expect(get(result, 'values.dependencies').length).toBe(2)
expect(get(result, 'warnings.dependencies.value')).toBe('a, b')
expect(get(result, 'values.project')).toBe('maven-project')
expect(get(result, 'values.language')).toBe('java')
expect(get(result, 'values.boot')).toBe(get(defaultValues, 'boot'))
expect(get(result, 'values.meta.packaging')).toBe('jar')
expect(get(result, 'values.meta.java')).toBe('1.8')
})
it('return parameters, no warning and error', () => {
const json = { ...MockClient }
const defaultValues = getDefaultValues(json)
const listsValues = getLists(json)
const result = parseParams(
defaultValues,
{
type: 'gradle-project',
language: 'kotlin',
architecture: 'none',
platformVersion: '1.1.1',
packaging: 'war',
jvmVersion: '11',
groupId: 'com.example2',
artifactId: 'demo2',
name: 'demo2',
description: 'Demo project for Spring Boot2',
packageName: 'com.example2.demo2',
dependencies: ' devtools , lombok ',
},
listsValues
)
expect(Object.keys(result.warnings).length).toBe(0)
expect(Object.keys(result.errors).length).toBe(1)
expect(get(result, 'values.project')).toBe('gradle-project')
expect(get(result, 'values.language')).toBe('kotlin')
expect(get(result, 'values.boot')).toBe(get(defaultValues, 'boot'))
expect(get(result, 'values.meta.packaging')).toBe('war')
expect(get(result, 'values.meta.java')).toBe('11')
expect(get(result, 'values.meta.group')).toBe('com.example2')
expect(get(result, 'values.meta.artifact')).toBe('demo2')
expect(get(result, 'values.meta.name')).toBe('demo2')
expect(get(result, 'values.meta.description')).toBe(
'Demo project for Spring Boot2'
)
expect(get(result, 'values.meta.packageName')).toBe('com.example2.demo2')
expect(get(result, 'values.dependencies').length).toBe(2)
expect(get(result, 'errors.boot.value')).toBe('1.1.1')
})
it('return parameters, warnings and error', () => {
const json = { ...MockClient }
const defaultValues = getDefaultValues(json)
const listsValues = getLists(json)
const result = parseParams(
defaultValues,
{
type: 'ant-project',
language: 'php',
architecture: 'none',
platformVersion: '1.1.1',
packaging: 'tar',
jvmVersion: '1',
groupId: 'com.example',
artifactId: 'demo',
name: 'demo',
description: 'Demo project for Spring Boot',
packageName: 'com.example.demo',
dependencies: 'a,devtools,b,lombok',
},
listsValues
)
expect(get(result, 'warnings.project.value')).toBe('ant-project')
expect(get(result, 'warnings.language.value')).toBe('php')
expect(get(result, 'warnings.meta.packaging.value')).toBe('tar')
expect(get(result, 'warnings.meta.java.value')).toBe('1')
expect(get(result, 'errors.boot.value')).toBe('1.1.1')
expect(get(result, 'values.meta.group')).toBe('com.example')
expect(get(result, 'values.meta.artifact')).toBe('demo')
expect(get(result, 'values.meta.name')).toBe('demo')
expect(get(result, 'values.meta.description')).toBe(
'Demo project for Spring Boot'
)
expect(get(result, 'values.dependencies').length).toBe(2)
expect(get(result, 'warnings.dependencies.value')).toBe('a, b')
expect(get(result, 'values.project')).toBe('maven-project')
expect(get(result, 'values.language')).toBe('java')
expect(get(result, 'values.boot')).toBe(get(defaultValues, 'boot'))
expect(get(result, 'values.meta.packaging')).toBe('jar')
expect(get(result, 'values.meta.java')).toBe('1.8')
})
})
/**
* Function getShareUrl
*/
describe('getShareUrl', () => {
it('return the base URL with custom values', () => {
const result = getShareUrl({
project: 'foo1',
language: 'foo2',
architecture: 'none',
boot: 'foo3',
meta: {
packaging: 'foo4',
java: 'foo5',
group: 'foo6',
artifact: 'foo7',
name: 'foo8',
description: 'foo9',
packageName: 'foo10',
},
dependencies: ['foo11', 'foo12'],
})
expect(result).toBe(
'type=foo1&language=foo2&architecture=none&platformVersion=foo3&packaging=foo4&jvmVersion=foo5&groupId=foo6&artifactId=foo7&name=foo8&description=foo9&packageName=foo10&dependencies=foo11,foo12'
)
})
})
/**
* Get Project
*/
describe('getProject', () => {
beforeEach(() => {
fetch.resetMocks()
fetch.mockResponseOnce(JSON.stringify({}))
})
it('should call the right service with the right parameters', () => {
const values = {
project: 'foo1',
language: 'foo2',
architecture: 'none',
boot: 'foo3',
meta: {
packaging: 'foo4',
java: 'foo5',
group: 'foo6',
artifact: 'foo7',
name: 'foo8',
description: 'foo9',
packageName: 'foo10',
},
dependencies: ['foo11', 'foo12'],
}
getProject('http://demo/starter.zip', values, [
{ id: 'foo11' },
{ id: 'foo12' },
])
expect(fetch.mock.calls.length).toEqual(1)
expect(fetch.mock.calls[0][0]).toEqual(
'http://demo/starter.zip?type=foo1&language=foo2&architecture=none&bootVersion=foo3&baseDir=foo7&groupId=foo6&artifactId=foo7&name=foo8&description=foo9&packageName=foo10&packaging=foo4&javaVersion=foo5&dependencies=foo11,foo12'
)
})
it('should call the right service with the right parameters (clean invalid dependencies)', () => {
const values = {
project: 'foo1',
language: 'foo2',
architecture: 'none',
boot: 'foo3',
meta: {
packaging: 'foo4',
java: 'foo5',
group: 'foo6',
artifact: 'foo7',
name: 'foo8',
description: 'foo9',
packageName: 'foo10',
},
dependencies: ['foo11', 'foo12'],
}
getProject('http://demo/starter.zip', values, [{ id: 'foo11' }])
expect(fetch.mock.calls.length).toEqual(1)
expect(fetch.mock.calls[0][0]).toEqual(
'http://demo/starter.zip?type=foo1&language=foo2&architecture=none&bootVersion=foo3&baseDir=foo7&groupId=foo6&artifactId=foo7&name=foo8&description=foo9&packageName=foo10&packaging=foo4&javaVersion=foo5&dependencies=foo11'
)
})
it('should call the right service with the right parameters (no dependency)', () => {
const values = {
project: 'foo1',
language: 'foo2',
architecture: 'none',
boot: 'foo3',
meta: {
packaging: 'foo4',
java: 'foo5',
group: 'foo6',
artifact: 'foo7',
name: 'foo8',
description: 'foo9',
packageName: 'foo10',
},
}
getProject('http://demo/starter.zip', values, [])
expect(fetch.mock.calls.length).toEqual(1)
expect(fetch.mock.calls[0][0]).toEqual(
'http://demo/starter.zip?type=foo1&language=foo2&architecture=none&bootVersion=foo3&baseDir=foo7&groupId=foo6&artifactId=foo7&name=foo8&description=foo9&packageName=foo10&packaging=foo4&javaVersion=foo5'
)
})
})

View File

@@ -0,0 +1,165 @@
import get from 'lodash.get'
import {
compare,
getValidDependencies,
isInRange,
parseQualifier,
parseReleases,
parseVersion,
rangeToText,
} from '../Version'
/**
* Function parseQualifier
*/
describe('parseQualifier', () => {
it('should return the default value (RELEASE)', () => {
let release = parseQualifier('1.0.0')
expect(release).toBe('RELEASE')
release = parseQualifier('1.0.0.RELEASE')
expect(release).toBe('RELEASE')
release = parseQualifier('foo')
expect(release).toBe('RELEASE')
})
it('should return a specific qualifier', () => {
let release = parseQualifier('1.0.0.M2')
expect(release).toBe('M')
release = parseQualifier('1.0.0.RC1')
expect(release).toBe('RC')
release = parseQualifier('1.0.0.BUILD-SNAPSHOT')
expect(release).toBe('BUILD-SNAPSHOT')
})
})
/**
* Function parseVersion
*/
describe('parseVersion', () => {
it('should parse correctly the version', () => {
const version = parseVersion('1.0.1.RELEASE')
expect(get(version, 'version')).toBe('1.0.1.RELEASE')
expect(get(version, 'short')).toBe('1.0.1')
expect(get(version, 'major')).toBe('1.0.x')
expect(get(version, 'qualify')).toBe(3)
expect(get(version, 'minor')).toBe(1)
})
it('should not parse the version', () => {
const version = parseVersion('foo')
expect(get(version, 'version')).toBe('foo')
expect(get(version, 'short', null)).toBeNull()
expect(get(version, 'major', null)).toBeNull()
expect(get(version, 'qualify', null)).toBeNull()
expect(get(version, 'minor', null)).toBeNull()
})
})
/**
* Function compare
*/
describe('compare', () => {
it('should compare 2 versions', () => {
let result = compare('1.0.0.RELEASE', '1.0.1.RELEASE')
expect(result < 0).toBe(true)
result = compare('1.0.2.RELEASE', '1.0.1.RELEASE')
expect(result > 0).toBe(true)
result = compare('1.0.1.RELEASE', '1.0.1.M1')
expect(result > 0).toBe(true)
result = compare('1.0.1.RELEASE', '1.0.1.RC1')
expect(result > 0).toBe(true)
result = compare('1.0.1.RELEASE', '1.0.1.BUILD-SNAPSHOT')
expect(result > 0).toBe(true)
result = compare('1.0.1', '1.0.1.BUILD-SNAPSHOT')
expect(result > 0).toBe(true)
result = compare('1.0.1.RELEASE', '1.0.1')
expect(result > 0).toBe(true)
result = compare('1.0.1', '1.0.1.RELEASE')
expect(result < 0).toBe(true)
})
})
/**
* Function parseReleases
*/
describe('parseReleases', () => {
it('should parse the versions', () => {
const result = parseReleases([{ key: '1.0.1.RELEASE' }])
expect(result.length).toBe(1)
expect(get(result[0], 'version')).toBe('1.0.1.RELEASE')
expect(get(result[0], 'short')).toBe('1.0.1')
expect(get(result[0], 'major')).toBe('1.0.x')
expect(get(result[0], 'qualify')).toBe(3)
expect(get(result[0], 'minor')).toBe(1)
})
})
/**
* Function isInRange
*/
describe('isInRange', () => {
it('should return true', () => {
let result = isInRange('2.1.1.RELEASE', '[2.0.0.RELEASE,2.2.0.M1)')
expect(result).toBe(true)
result = isInRange('2.0.0.RELEASE', '[2.0.0.RELEASE,2.2.0.M1)')
expect(result).toBe(true)
result = isInRange('2.0.0.RELEASE', '')
expect(result).toBe(true)
})
it('should return false', () => {
let result = isInRange('1.9.8.RELEASE', '[2.0.0.RELEASE,2.2.0.M1)')
expect(result).toBe(false)
result = isInRange('2.0.0.M1', '[2.0.0.RELEASE,2.2.0.M1)')
expect(result).toBe(false)
})
})
/**
* Function rangeToText
*/
describe('rangeToText', () => {
it('should return the correct string', () => {
let result = rangeToText('[2.0.0.RELEASE,2.2.0.M1)')
expect(result).toBe('>= 2.0.0.RELEASE and < 2.2.0.M1')
result = rangeToText('[2.0.0.RELEASE,2.2.0.M1]')
expect(result).toBe('>= 2.0.0.RELEASE and <= 2.2.0.M1')
result = rangeToText('(2.0.0.RELEASE,2.2.0.M1]')
expect(result).toBe('> 2.0.0.RELEASE and <= 2.2.0.M1')
result = rangeToText('2.0.0.RELEASE')
expect(result).toBe('>= 2.0.0.RELEASE')
})
})
/**
* Function getValidDependencies
*/
describe('getValidDependencies', () => {
it('should return the valid dependencies', () => {
const result = getValidDependencies('2.0.0.RELEASE', [
{
name: 'foo1',
versionRange: '[2.0.0.RELEASE,2.2.0.M1)',
},
{
name: 'foo2',
versionRange: '(2.0.0.RELEASE,2.2.0.M1]',
},
{
name: 'foo3',
versionRange: '2.0.0.RELEASE',
},
{
name: 'foo4',
versionRange: '',
},
{
name: 'foo5',
},
])
expect(result.length).toBe(4)
expect(get(result[0], 'name')).toBe('foo1')
expect(get(result[1], 'name')).toBe('foo3')
expect(get(result[2], 'name')).toBe('foo4')
expect(get(result[3], 'name')).toBe('foo5')
})
})

View File

@@ -0,0 +1,81 @@
import JSZip from 'jszip'
import fs from 'fs'
import get from 'lodash.get'
import path from 'path'
import {createTree, findRoot, getLanguage} from '../Zip'
/**
* Function getLanguage
*/
describe('getLanguage', () => {
it('should return the extention', () => {
expect(getLanguage('index.md')).toBe('markdown')
expect(getLanguage('index.spec.md')).toBe('markdown')
expect(getLanguage('index.js')).toBe('javascript')
expect(getLanguage('index.kt')).toBe('kotlin')
expect(getLanguage('index.kts')).toBe('kotlin')
expect(getLanguage('index.gradle')).toBe('groovy')
expect(getLanguage('index.gitignore')).toBe('git')
expect(getLanguage('index.java')).toBe('java')
expect(getLanguage('index.xml')).toBe('xml')
expect(getLanguage('index.properties')).toBe('properties')
expect(getLanguage('index.groovy')).toBe('groovy')
expect(getLanguage('index.html')).toBe('html')
expect(getLanguage('index.css')).toBe('css')
})
it('should not return the extention', () => {
expect(getLanguage('index.php')).toBe(null)
expect(getLanguage('index.scss')).toBe(null)
expect(getLanguage('mvnw')).toBe(null)
expect(getLanguage('command.cmd')).toBe(null)
})
})
/**
* Function getLanguage
*/
describe('createTree', () => {
// eslint-disable-next-line
it('should create a tree', async done => {
const zipJs = new JSZip()
await fs.readFile(path.resolve('./dev/starter.zip'), async (err, blob) => {
const { files } = await zipJs.loadAsync(blob).catch(() => {
throw Error(`Could not load the ZIP project.`)
})
const pathZ = `${findRoot({ files })}/`
const result = await createTree(files, pathZ, pathZ, zipJs).catch(() => {
throw Error(`Could not read the ZIP project.`)
})
expect(get(result, 'tree', null) !== null).toBe(true)
expect(get(result, 'tree.type', null)).toBe('folder')
expect(get(result, 'tree.filename', null)).toBe('demo')
expect(get(result, 'tree.path', null)).toBe('/demo/')
expect(get(result, 'tree.hidden', null)).toBe(null)
expect(get(result, 'tree.children').length).toBe(7)
expect(get(result, 'selected.type')).toBe('file')
expect(get(result, 'selected.filename')).toBe('pom.xml')
expect(get(result, 'selected.path')).toBe('/demo/pom.xml')
expect(get(result, 'selected.language')).toBe('xml')
done()
})
})
})
/**
* Function getLanguage
*/
describe('findRoot', () => {
// eslint-disable-next-line
it('should return the right value', async done => {
const zipJs = new JSZip()
await fs.readFile(path.resolve('./dev/starter.zip'), async (err, blob) => {
const { files } = await zipJs.loadAsync(blob).catch(() => {
throw Error(`Could not load the ZIP project.`)
})
const pathZ = `${findRoot({ files })}/`
expect(pathZ).toBe('demo/')
done()
})
})
})