rewrite plugins mgmt page with react

This commit is contained in:
Pig Fang 2020-01-21 10:22:20 +08:00
parent 640a714d8e
commit cd3260af3e
20 changed files with 768 additions and 41 deletions

View File

@ -88,6 +88,7 @@ jobs:
run: |
yarn lint
yarn tsc -p . --noEmit
yarn tsc -p ./resources/assets/tests --noEmit
jest:
name: Frontend Tests
runs-on: ubuntu-latest

View File

@ -18,10 +18,14 @@
"dependencies": {
"@babel/runtime": "^7.8.0",
"@fortawesome/fontawesome-free": "^5.12.0",
"@hot-loader/react-dom": "^16.11.0",
"@tweenjs/tween.js": "^18.4.2",
"admin-lte": "^3.0.1",
"echarts": "^4.6.0",
"jquery": "^3.4.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.12.18",
"rxjs": "^6.5.3",
"skinview3d": "^1.2.1",
"vue": "^2.6.11",
@ -35,10 +39,13 @@
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.2",
"@gplane/tsconfig": "^1.0.0",
"@testing-library/react": "^9.4.0",
"@types/bootstrap": "^4.3.1",
"@types/echarts": "^4.4.2",
"@types/jest": "^24.0.25",
"@types/jquery": "^3.3.29",
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
"@types/webpack": "^4.41.2",
"@typescript-eslint/eslint-plugin": "^2.8.0",
"@typescript-eslint/parser": "^2.8.0",
@ -188,12 +195,13 @@
"js",
"vue",
"ts",
"tsx",
"json",
"node"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/resources/assets/src/$1",
"\\.css$": "<rootDir>/resources/assets/tests/__mocks__/style.ts",
"\\.(css|styl)$": "<rootDir>/resources/assets/tests/__mocks__/style.ts",
"\\.(png|jpg)$": "<rootDir>/resources/assets/tests/__mocks__/file.ts"
},
"setupFilesAfterEnv": [
@ -204,6 +212,15 @@
"<rootDir>/resources/assets/tests/setup",
"<rootDir>/resources/assets/tests/utils"
],
"testRegex": "resources/assets/tests/.*\\.(spec|test)\\.(t|j)s$"
"testMatch": [
"<rootDir>/resources/assets/tests/**/*.test.ts",
"<rootDir>/resources/assets/tests/**/*.test.tsx"
],
"globals": {
"ts-jest": {
"tsConfig": "<rootDir>/resources/assets/tests/tsconfig.json",
"isolatedModules": true
}
}
}
}

View File

@ -0,0 +1,9 @@
import React from 'react'
const Loading = () => (
<div className="container text-center" title="Loading...">
<i className="fas fa-sync fa-spin"></i>
</div>
)
export default Loading

View File

@ -1,6 +1,9 @@
import Vue from 'vue'
import * as React from 'react'
import ReactDOM from 'react-dom'
import './scripts/app'
import routes from './scripts/route'
import Loading from './components/Loading'
Vue.config.productionTip = false
@ -15,6 +18,17 @@ function loadModules() {
if (route.module) {
Promise.all(route.module.map(m => m()))
}
if (route.react) {
const Component = React.lazy(
route.react as (() => Promise<{ default: React.ComponentType }>)
)
const Root = () => (
<React.Suspense fallback={<Loading />}>
<Component />
</React.Suspense>
)
ReactDOM.render(<Root />, document.querySelector(route.el))
}
if (route.component) {
Vue.prototype.$route = new RegExp(`^${route.path}$`, 'i').exec(blessing.route)
// eslint-disable-next-line no-new

View File

@ -44,5 +44,4 @@ Vue.use(_Vue => {
})
})
// eslint-disable-next-line @typescript-eslint/unbound-method
window.trans = trans
Object.assign(window, { trans })

View File

@ -4,9 +4,10 @@ import { queryStringify } from './utils'
import { showModal } from './notify'
import { trans } from './i18n'
export interface ResponseBody {
export interface ResponseBody<T = null> {
code: number
message: string
data: T extends null ? never : T
}
class HTTPError extends Error {

View File

@ -78,7 +78,7 @@ export default [
},
{
path: 'admin/plugins/manage',
component: () => import('../views/admin/Plugins.vue'),
react: () => import('../views/admin/PluginsManagement'),
el: '.content > .container-fluid',
},
{

View File

@ -1,25 +1,22 @@
import Vue from 'vue'
import * as JQuery from 'jquery'
import JQuery from 'jquery'
import { ModalOptions, ModalResult } from './scripts/modal'
import { Toast } from './scripts/toast'
type I18n = 'en' | 'zh_CN'
declare global {
// eslint-disable-next-line no-redeclare
let blessing: {
base_url: string
debug: boolean
env: string
fallback_locale: I18n
locale: I18n
fallback_locale: string
locale: string
site_name: string
timezone: string
version: string
route: string
extra: any
i18n: object
ui: object
fetch: {
get(url: string, params?: object): Promise<object>

View File

@ -0,0 +1,31 @@
.box
cursor default
transition-property box-shadow
transition-duration 0.3s
width 32%
@media (max-width: 1280px)
width 47%
@media (max-width: 768px)
width 100%
&:hover
box-shadow 0 .5rem 1rem rgba(0,0,0,.15)
.content
max-width 85%
.actions
margin-top -7px
a
transition-property color
transition-duration 0.3s
color #000
&:hover
color #999
&:not(:last-child)
margin-right 9px
.description
font-size 14px
white-space nowrap
overflow hidden
text-overflow ellipsis

View File

@ -0,0 +1,88 @@
import React, { useCallback } from 'react'
import { trans } from '../../../scripts/i18n'
import { Plugin } from './types'
import styles from './InfoBox.styl'
interface Props {
plugin: Plugin
onEnable(plugin: Plugin): void
onDisable(plugin: Plugin): void
onDelete(plugin: Plugin): void
baseUrl: string
}
const InfoBox: React.FC<Props> = props => {
const { plugin } = props
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault()
if (event.target.checked) {
props.onEnable(plugin)
} else {
props.onDisable(plugin)
}
},
[],
)
const handleDelete = useCallback(() => {
props.onDelete(plugin)
}, [])
return (
<div className={`info-box mr-3 ${styles.box}`}>
<span className={`info-box-icon bg-${plugin.icon.bg}`}>
<i className={`${plugin.icon.faType} fa-${plugin.icon.fa}`} />
</span>
<div className={`info-box-content ${styles.content}`}>
<div className="d-flex justify-content-between">
<div>
<input
className="mr-2"
type="checkbox"
checked={plugin.enabled}
title={
plugin.enabled
? trans('admin.disablePlugin')
: trans('admin.enablePlugin')
}
onChange={handleChange}
/>
<strong className="mr-2">{plugin.title}</strong>
<span className="text-gray">v{plugin.version}</span>
</div>
<div className={styles.actions}>
{plugin.readme && (
<a
href={`${props.baseUrl}/admin/plugins/readme/${plugin.name}`}
title={trans('admin.pluginReadme')}
>
<i className="fas fa-question" />
</a>
)}
{plugin.enabled && plugin.config && (
<a
href={`${props.baseUrl}/admin/plugins/config/${plugin.name}`}
title={trans('admin.configurePlugin')}
>
<i className="fas fa-cog" />
</a>
)}
<a
href="#"
title={trans('admin.deletePlugin')}
onClick={handleDelete}
>
<i className="fas fa-trash" />
</a>
</div>
</div>
<div className={`mt-2 ${styles.description}`}>{plugin.description}</div>
</div>
</div>
)
}
export default React.memo(InfoBox)

View File

@ -0,0 +1,103 @@
import React, { useState, useEffect, useCallback } from 'react'
import { hot } from 'react-hot-loader/root'
import { trans } from '../../../scripts/i18n'
import * as fetch from '../../../scripts/net'
import { toast, showModal } from '../../../scripts/notify'
import Loading from '../../../components/Loading'
import alertUnresolved from '../../../components/mixins/alertUnresolvedPlugins'
import InfoBox from './InfoBox'
import { Plugin } from './types'
const PluginsManagement: React.FC = () => {
const [loading, setLoading] = useState(false)
const [plugins, setPlugins] = useState<Plugin[]>([])
useEffect(() => {
const getPlugins = async () => {
setLoading(true)
setPlugins(await fetch.get('/admin/plugins/data'))
setLoading(false)
}
getPlugins()
}, [])
const handleEnable = useCallback(async (plugin: Plugin, i: number) => {
const {
code,
message,
data: { reason } = { reason: [] },
}: fetch.ResponseBody<{
reason: string[]
}> = await fetch.post('/admin/plugins/manage', {
action: 'enable',
name: plugin.name,
})
if (code === 0) {
toast.success(message)
setPlugins(plugins => {
plugins.splice(i, 1, { ...plugin, enabled: true })
return plugins.slice()
})
} else {
alertUnresolved(message, reason)
}
}, [])
const handleDisable = useCallback(async (plugin: Plugin, i: number) => {
const { code, message } = await fetch.post('/admin/plugins/manage', {
action: 'disable',
name: plugin.name,
})
if (code === 0) {
toast.success(message)
setPlugins(plugins => {
plugins.splice(i, 1, { ...plugin, enabled: false })
return plugins.slice()
})
} else {
toast.error(message)
}
}, [])
const handleDelete = useCallback(async (plugin: Plugin) => {
try {
await showModal({
title: plugin.title,
text: trans('admin.confirmDeletion'),
okButtonType: 'danger',
})
} catch {
return
}
const { code, message } = await fetch.post('/admin/plugins/manage', {
action: 'delete',
name: plugin.name,
})
if (code === 0) {
const { name } = plugin
setPlugins(plugins => plugins.filter(plugin => plugin.name !== name))
toast.success(message)
} else {
toast.error(message)
}
}, [])
return loading ? (
<Loading />
) : (
<div className="d-flex flex-wrap">
{plugins.map((plugin, i) => (
<InfoBox
key={plugin.name}
plugin={plugin}
onEnable={plugin => handleEnable(plugin, i)}
onDisable={plugin => handleDisable(plugin, i)}
onDelete={handleDelete}
baseUrl={blessing.base_url}
/>
))}
</div>
)
}
export default hot(PluginsManagement)

View File

@ -0,0 +1,10 @@
export type Plugin = {
name: string
title: string
description: string
version: string
enabled: boolean
config: boolean
readme: boolean
icon: { fa: string; faType: 'fas' | 'fab'; bg: string }
}

9
resources/assets/src/webpack.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
declare module '*.styl' {
export default {} as Record<string, string>
}

View File

@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": "..",
"paths": {
"@/scripts/*": [
"./tests/ts-shims/*",
"./src/scripts/*"
],
"@/components/*": [
"./src/components/*"
],
"@/views/*": [
"./src/views/*"
]
}
},
"include": [
"."
]
}

View File

@ -1,4 +1,4 @@
class Request {
declare class Request {
url: string
headers: Map<string, string>
@ -19,3 +19,15 @@ interface Window {
$: jest.Mock
}
declare let blessing: {
base_url: string
site_name: string
timezone: string
version: string
route: string
extra: any
i18n: object
fetch: any
event: any
}

View File

@ -0,0 +1,204 @@
import React from 'react'
import { render, wait, fireEvent } from '@testing-library/react'
import { trans } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { showModal, toast } from '@/scripts/notify'
import PluginsManagement from '@/views/admin/PluginsManagement'
jest.mock('@/scripts/net')
jest.mock('@/scripts/notify')
test('show loading indicator', () => {
fetch.get.mockResolvedValue([])
const { queryByTitle } = render(<PluginsManagement />)
expect(queryByTitle('Loading...')).not.toBeNull()
})
test('plugin info box', async () => {
fetch.get.mockResolvedValue([
{
name: 'a',
title: 'My Plugin',
version: '1.0.0',
description: 'desc',
config: true,
readme: true,
icon: {},
enabled: true,
},
])
const { queryByTitle, queryByText } = render(<PluginsManagement />)
await wait()
expect(queryByTitle(trans('admin.configurePlugin'))).not.toBeNull()
expect(queryByTitle(trans('admin.pluginReadme'))).not.toBeNull()
expect(queryByText('My Plugin')).not.toBeNull()
expect(queryByText('v1.0.0')).not.toBeNull()
expect(queryByText('desc')).not.toBeNull()
})
describe('enable plugin', () => {
beforeEach(() => {
fetch.get.mockResolvedValue([
{
name: 'a',
icon: {},
enabled: false,
},
])
})
it('successfully', async () => {
fetch.get.mockResolvedValue([
{
name: 'a',
icon: {},
enabled: false,
},
])
fetch.post.mockResolvedValue({ code: 0, message: '0' })
const { getByTitle } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.enablePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'enable',
name: 'a',
})
expect(toast.success).toBeCalled()
})
it('failed', async () => {
fetch.post.mockResolvedValue({
code: 1,
message: '1',
data: { reason: ['abc'] },
})
const { getByTitle } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.enablePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'enable',
name: 'a',
})
expect(showModal).toBeCalledWith({
mode: 'alert',
dangerousHTML: expect.stringContaining('<li>abc</li>'),
})
})
})
describe('disable plugin', () => {
beforeEach(() => {
fetch.get.mockResolvedValue([
{
name: 'a',
icon: {},
enabled: true,
},
])
})
it('successfully', async () => {
fetch.post.mockResolvedValue({ code: 0, message: '0' })
const { getByTitle } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.disablePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'disable',
name: 'a',
})
expect(toast.success).toBeCalledWith('0')
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: '1' })
const { getByTitle } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.disablePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'disable',
name: 'a',
})
expect(toast.error).toBeCalledWith('1')
})
})
describe('delete plugin', () => {
beforeEach(() => {
fetch.get.mockResolvedValue([
{
name: 'a',
title: 'My Plugin',
icon: {},
enabled: false,
},
])
})
it('rejected by user', async () => {
showModal.mockRejectedValue({})
const { getByTitle } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
await wait()
expect(showModal).toBeCalledWith({
title: 'My Plugin',
text: trans('admin.confirmDeletion'),
okButtonType: 'danger',
})
expect(fetch.post).not.toBeCalled()
})
it('successfully', async () => {
showModal.mockResolvedValue({ value: '' })
fetch.post.mockResolvedValue({ code: 0, message: '0' })
const { getByTitle, queryByText } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'delete',
name: 'a',
})
expect(toast.success).toBeCalledWith('0')
expect(queryByText('My Plugin')).toBeNull()
})
it('failed', async () => {
showModal.mockResolvedValue({ value: '' })
fetch.post.mockResolvedValue({ code: 1, message: '1' })
const { getByTitle, queryByText } = render(<PluginsManagement />)
await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
action: 'delete',
name: 'a',
})
expect(toast.error).toBeCalledWith('1')
expect(queryByText('My Plugin')).not.toBeNull()
})
})

View File

@ -5,9 +5,5 @@ declare module '*.vue' {
}
declare module '*.styl' {
export default {}
}
declare module '*.yml' {
export default {}
export default {} as Record<string, string>
}

View File

@ -5,22 +5,10 @@
"module": "esnext",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/scripts/*": [
"./resources/assets/tests/ts-shims/*",
"./resources/assets/src/scripts/*"
],
"@/components/*": [
"./resources/assets/src/components/*"
],
"@/views/*": [
"./resources/assets/src/views/*"
]
}
"incremental": true
},
"exclude": [
"node_modules",
"plugins",
"vendor"
"include": [
"resources/assets/src",
"resources/assets/webpack.d.ts"
]
}

View File

@ -12,7 +12,7 @@ const devMode = !process.argv.includes('-p')
const config = {
mode: devMode ? 'development' : 'production',
entry: {
app: './resources/assets/src/index.ts',
app: ['react-hot-loader/patch', './resources/assets/src/index.tsx'],
'language-chooser': './resources/assets/src/scripts/language-chooser.ts',
style: [
'bootstrap/dist/css/bootstrap.min.css',
@ -46,8 +46,7 @@ const config = {
use: ['cache-loader', 'vue-loader'],
},
{
test: /\.(css|styl(us)?)$/,
exclude: /node_modules/,
test: /\.vue.*\.stylus$/,
use: [
'vue-style-loader',
{ loader: 'css-loader', options: { importLoaders: 2 } },
@ -55,6 +54,25 @@ const config = {
'stylus-loader',
],
},
{
test: /views.*\.styl$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: {
localIdentName: devMode ? '[name]__[local]' : '[hash:base64]',
},
localsConvention: 'dashes',
esModule: true,
},
},
'postcss-loader',
'stylus-loader',
],
},
{
test: /(node_modules.*)\.css$/,
use: [
@ -93,7 +111,10 @@ const config = {
}),
],
resolve: {
extensions: ['.js', '.ts', '.vue', '.json'],
extensions: ['.js', '.ts', '.tsx', '.vue', '.json'],
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

212
yarn.lock
View File

@ -828,6 +828,13 @@
levenary "^1.1.0"
semver "^5.5.0"
"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1"
integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.0.tgz#8c81711517c56b3d00c6de706b0fb13dc3531549"
@ -960,6 +967,16 @@
resolved "https://registry.npmjs.org/@gplane/tsconfig/-/tsconfig-1.0.0.tgz#c7e6d56ece3d045abe46d00aece990d8412440f0"
integrity sha512-x0Lxt+oBXGVK8qYYLQvohFYQZlt9KgeVfkGAEXWOq38Gxu5KdcsGzPvyWPMhz5Og/U2pWXbjw4+rN4Qa7CPiWQ==
"@hot-loader/react-dom@^16.11.0":
version "16.11.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd"
integrity sha512-cIOVB8YgT4EVCNiXK+gGuYl6adW/TKlW3N7GvgY5QgpL2NTWagF/oJxVscHSdR3O7NjJsoxdseB5spqwCHNIhA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.17.0"
"@jest/console@^24.7.1":
version "24.7.1"
resolved "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
@ -1131,11 +1148,37 @@
resolved "https://registry.yarnpkg.com/@lgaitan/pace-progress/-/pace-progress-1.0.7.tgz#c96fbbd9fd4cf528feed34ea0c8f9d8b3e98f0dd"
integrity sha1-yW+72f1M9Sj+7TTqDI+diz6Y8N0=
"@sheerun/mutationobserver-shim@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
"@sweetalert2/theme-bootstrap-4@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@sweetalert2/theme-bootstrap-4/-/theme-bootstrap-4-2.2.1.tgz#a0e3496f2d5aa2993f8fe1c59dcec0673941cddb"
integrity sha512-EzAc/HFO16wuZCmawdv0mxRknXtQ5XYmms8gHCcRBqVJsolwl0xKanH8wC2tf4O6dFLou6ZndNqii8ArQO66pA==
"@testing-library/dom@^6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.11.0.tgz#962a38f1a721fdb7c9e35e7579e33ff13a00eda4"
integrity sha512-Pkx9LMIGshyNbfmecjt18rrAp/ayMqGH674jYER0SXj0iG9xZc+zWRjk2Pg9JgPBDvwI//xGrI/oOQkAi4YEew==
dependencies:
"@babel/runtime" "^7.6.2"
"@sheerun/mutationobserver-shim" "^0.3.2"
"@types/testing-library__dom" "^6.0.0"
aria-query "3.0.0"
pretty-format "^24.9.0"
wait-for-expect "^3.0.0"
"@testing-library/react@^9.4.0":
version "9.4.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.4.0.tgz#b021ac8cb987c8dc54c6841875f745bf9b2e88e5"
integrity sha512-XdhDWkI4GktUPsz0AYyeQ8M9qS/JFie06kcSnUVcpgOwFjAu9vhwR83qBl+lw9yZWkbECjL8Hd+n5hH6C0oWqg==
dependencies:
"@babel/runtime" "^7.7.6"
"@testing-library/dom" "^6.11.0"
"@types/testing-library__react" "^9.1.2"
"@ttskch/select2-bootstrap4-theme@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@ttskch/select2-bootstrap4-theme/-/select2-bootstrap4-theme-1.3.2.tgz#c9e17e34fb2cfd9f41b8efe3584165ccbdd96969"
@ -1274,11 +1317,31 @@
resolved "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz#e19436e7f8e9b4601005d73673b6dc4784ffcc2f"
integrity sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/q@^1.5.1":
version "1.5.2"
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/react-dom@*", "@types/react-dom@^16.9.4":
version "16.9.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df"
integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.17":
version "16.9.17"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e"
integrity sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/sizzle@*":
version "2.3.2"
resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
@ -1299,6 +1362,21 @@
resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
"@types/testing-library__dom@*", "@types/testing-library__dom@^6.0.0":
version "6.11.1"
resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.11.1.tgz#6058a6ac391db679f7c60dbb27b81f0620de2dd9"
integrity sha512-ImChHtQqmjwraRLqBC2sgSQFtczeFvBmBcfhTYZn/3KwXbyD07LQykEQ0xJo7QHc1GbVvf7pRyGaIe6PkCdxEw==
dependencies:
pretty-format "^24.3.0"
"@types/testing-library__react@^9.1.2":
version "9.1.2"
resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.2.tgz#e33af9124c60a010fc03a34eff8f8a34a75c4351"
integrity sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q==
dependencies:
"@types/react-dom" "*"
"@types/testing-library__dom" "*"
"@types/uglify-js@*":
version "3.0.4"
resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@ -1842,6 +1920,14 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
aria-query@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=
dependencies:
ast-types-flow "0.0.7"
commander "^2.11.0"
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -1937,6 +2023,11 @@ ast-transform@0.0.0:
esprima "~1.0.4"
through "~2.3.4"
ast-types-flow@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
ast-types@^0.7.0:
version "0.7.8"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.7.8.tgz#902d2e0d60d071bdcd46dc115e1809ed11c138a9"
@ -2734,6 +2825,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.11.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^2.20.0, commander@~2.20.0:
version "2.20.0"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
@ -3171,6 +3267,11 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
csstype@^2.2.0:
version "2.6.8"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431"
integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA==
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -3657,6 +3758,11 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@ -4358,7 +4464,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@ -4790,6 +4896,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
global@^4.3.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -4947,6 +5061,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f"
integrity sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==
dependencies:
react-is "^16.7.0"
homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
@ -6394,7 +6515,7 @@ loglevel@^1.6.6:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312"
integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==
loose-envify@^1.0.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -6626,6 +6747,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
dependencies:
dom-walk "^0.1.0"
mini-css-extract-plugin@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
@ -7938,7 +8066,7 @@ pretty-format@^22.4.3:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
pretty-format@^24.9.0:
pretty-format@^24.3.0, pretty-format@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
@ -7985,6 +8113,15 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
prop-types@^15.6.1, prop-types@^15.6.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
proxy-addr@~2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
@ -8154,11 +8291,54 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dom@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11"
integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.18.0"
react-hot-loader@^4.12.18:
version "4.12.18"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7"
integrity sha512-qYD0Qi9lIbg9jLyfmodfqvAQqCBsoPKxAhca8Nxvy2/2pO5Q9r2kM28jN0bbbSnhwK8dJ7FjsfVtXKOxMW+bqw==
dependencies:
fast-levenshtein "^2.0.6"
global "^4.3.0"
hoist-non-react-statics "^3.3.0"
loader-utils "^1.1.0"
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
shallowequal "^1.1.0"
source-map "^0.7.3"
react-is@^16.7.0, react-is@^16.8.1:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-is@^16.8.4:
version "16.8.6"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"
integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
read-pkg-up@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
@ -8564,6 +8744,22 @@ sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.17.0:
version "0.17.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe"
integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@ -8743,6 +8939,11 @@ shallow-copy@~0.0.1:
resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=
shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -10019,6 +10220,11 @@ w3c-hr-time@^1.0.1:
dependencies:
browser-process-hrtime "^0.1.2"
wait-for-expect@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.1.tgz#ec204a76b0038f17711e575720aaf28505ac7185"
integrity sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw==
walker@^1.0.7, walker@~1.0.5:
version "1.0.7"
resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"