From cd3260af3e0950805a68c91789e496c2b31dfcf1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 21 Jan 2020 10:22:20 +0800 Subject: [PATCH 01/58] rewrite plugins mgmt page with react --- .github/workflows/CI.yml | 1 + package.json | 21 +- resources/assets/src/components/Loading.tsx | 9 + resources/assets/src/{index.ts => index.tsx} | 14 ++ resources/assets/src/scripts/i18n.ts | 3 +- resources/assets/src/scripts/net.ts | 3 +- resources/assets/src/scripts/route.ts | 2 +- resources/assets/src/shims.d.ts | 9 +- .../admin/PluginsManagement/InfoBox.styl | 31 +++ .../views/admin/PluginsManagement/InfoBox.tsx | 88 ++++++++ .../views/admin/PluginsManagement/index.tsx | 103 +++++++++ .../views/admin/PluginsManagement/types.ts | 10 + resources/assets/src/webpack.d.ts | 9 + resources/assets/tests/tsconfig.json | 21 ++ .../assets/tests/{scripts => }/types.d.ts | 14 +- .../views/admin/PluginsManagement.test.tsx | 204 +++++++++++++++++ resources/assets/tests/webpack.d.ts | 6 +- tsconfig.json | 20 +- webpack.config.js | 29 ++- yarn.lock | 212 +++++++++++++++++- 20 files changed, 768 insertions(+), 41 deletions(-) create mode 100644 resources/assets/src/components/Loading.tsx rename resources/assets/src/{index.ts => index.tsx} (58%) create mode 100644 resources/assets/src/views/admin/PluginsManagement/InfoBox.styl create mode 100644 resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx create mode 100644 resources/assets/src/views/admin/PluginsManagement/index.tsx create mode 100644 resources/assets/src/views/admin/PluginsManagement/types.ts create mode 100644 resources/assets/src/webpack.d.ts create mode 100644 resources/assets/tests/tsconfig.json rename resources/assets/tests/{scripts => }/types.d.ts (57%) create mode 100644 resources/assets/tests/views/admin/PluginsManagement.test.tsx diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ad3800eb..3a89eadf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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 diff --git a/package.json b/package.json index dad98a42..b0ab853f 100644 --- a/package.json +++ b/package.json @@ -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": { "^@/(.*)$": "/resources/assets/src/$1", - "\\.css$": "/resources/assets/tests/__mocks__/style.ts", + "\\.(css|styl)$": "/resources/assets/tests/__mocks__/style.ts", "\\.(png|jpg)$": "/resources/assets/tests/__mocks__/file.ts" }, "setupFilesAfterEnv": [ @@ -204,6 +212,15 @@ "/resources/assets/tests/setup", "/resources/assets/tests/utils" ], - "testRegex": "resources/assets/tests/.*\\.(spec|test)\\.(t|j)s$" + "testMatch": [ + "/resources/assets/tests/**/*.test.ts", + "/resources/assets/tests/**/*.test.tsx" + ], + "globals": { + "ts-jest": { + "tsConfig": "/resources/assets/tests/tsconfig.json", + "isolatedModules": true + } + } } } diff --git a/resources/assets/src/components/Loading.tsx b/resources/assets/src/components/Loading.tsx new file mode 100644 index 00000000..980ba246 --- /dev/null +++ b/resources/assets/src/components/Loading.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Loading = () => ( +
+ +
+) + +export default Loading diff --git a/resources/assets/src/index.ts b/resources/assets/src/index.tsx similarity index 58% rename from resources/assets/src/index.ts rename to resources/assets/src/index.tsx index c3c5e8c1..75529c1f 100644 --- a/resources/assets/src/index.ts +++ b/resources/assets/src/index.tsx @@ -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 = () => ( + }> + + + ) + ReactDOM.render(, document.querySelector(route.el)) + } if (route.component) { Vue.prototype.$route = new RegExp(`^${route.path}$`, 'i').exec(blessing.route) // eslint-disable-next-line no-new diff --git a/resources/assets/src/scripts/i18n.ts b/resources/assets/src/scripts/i18n.ts index 47bfea27..8630e0d3 100644 --- a/resources/assets/src/scripts/i18n.ts +++ b/resources/assets/src/scripts/i18n.ts @@ -44,5 +44,4 @@ Vue.use(_Vue => { }) }) -// eslint-disable-next-line @typescript-eslint/unbound-method -window.trans = trans +Object.assign(window, { trans }) diff --git a/resources/assets/src/scripts/net.ts b/resources/assets/src/scripts/net.ts index 7ec61a58..c0af72f2 100644 --- a/resources/assets/src/scripts/net.ts +++ b/resources/assets/src/scripts/net.ts @@ -4,9 +4,10 @@ import { queryStringify } from './utils' import { showModal } from './notify' import { trans } from './i18n' -export interface ResponseBody { +export interface ResponseBody { code: number message: string + data: T extends null ? never : T } class HTTPError extends Error { diff --git a/resources/assets/src/scripts/route.ts b/resources/assets/src/scripts/route.ts index 2994ed15..f2dfad30 100644 --- a/resources/assets/src/scripts/route.ts +++ b/resources/assets/src/scripts/route.ts @@ -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', }, { diff --git a/resources/assets/src/shims.d.ts b/resources/assets/src/shims.d.ts index fca61508..707fd334 100644 --- a/resources/assets/src/shims.d.ts +++ b/resources/assets/src/shims.d.ts @@ -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 diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl b/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl new file mode 100644 index 00000000..5d11ef8d --- /dev/null +++ b/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl @@ -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 diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx new file mode 100644 index 00000000..af87ac8a --- /dev/null +++ b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx @@ -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 => { + const { plugin } = props + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + event.preventDefault() + + if (event.target.checked) { + props.onEnable(plugin) + } else { + props.onDisable(plugin) + } + }, + [], + ) + + const handleDelete = useCallback(() => { + props.onDelete(plugin) + }, []) + + return ( +
+ + + +
+
+
+ + {plugin.title} + v{plugin.version} +
+
+ {plugin.readme && ( + + + + )} + {plugin.enabled && plugin.config && ( + + + + )} + + + +
+
+
{plugin.description}
+
+
+ ) +} + +export default React.memo(InfoBox) diff --git a/resources/assets/src/views/admin/PluginsManagement/index.tsx b/resources/assets/src/views/admin/PluginsManagement/index.tsx new file mode 100644 index 00000000..f09b1911 --- /dev/null +++ b/resources/assets/src/views/admin/PluginsManagement/index.tsx @@ -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([]) + 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 ? ( + + ) : ( +
+ {plugins.map((plugin, i) => ( + handleEnable(plugin, i)} + onDisable={plugin => handleDisable(plugin, i)} + onDelete={handleDelete} + baseUrl={blessing.base_url} + /> + ))} +
+ ) +} + +export default hot(PluginsManagement) diff --git a/resources/assets/src/views/admin/PluginsManagement/types.ts b/resources/assets/src/views/admin/PluginsManagement/types.ts new file mode 100644 index 00000000..3ebf9d04 --- /dev/null +++ b/resources/assets/src/views/admin/PluginsManagement/types.ts @@ -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 } +} diff --git a/resources/assets/src/webpack.d.ts b/resources/assets/src/webpack.d.ts new file mode 100644 index 00000000..8c582452 --- /dev/null +++ b/resources/assets/src/webpack.d.ts @@ -0,0 +1,9 @@ +declare module '*.vue' { + import Vue from 'vue' + + export default Vue +} + +declare module '*.styl' { + export default {} as Record +} diff --git a/resources/assets/tests/tsconfig.json b/resources/assets/tests/tsconfig.json new file mode 100644 index 00000000..5da8e1c0 --- /dev/null +++ b/resources/assets/tests/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": "..", + "paths": { + "@/scripts/*": [ + "./tests/ts-shims/*", + "./src/scripts/*" + ], + "@/components/*": [ + "./src/components/*" + ], + "@/views/*": [ + "./src/views/*" + ] + } + }, + "include": [ + "." + ] +} diff --git a/resources/assets/tests/scripts/types.d.ts b/resources/assets/tests/types.d.ts similarity index 57% rename from resources/assets/tests/scripts/types.d.ts rename to resources/assets/tests/types.d.ts index 126a4ecd..38195e1b 100644 --- a/resources/assets/tests/scripts/types.d.ts +++ b/resources/assets/tests/types.d.ts @@ -1,4 +1,4 @@ -class Request { +declare class Request { url: string headers: Map @@ -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 +} diff --git a/resources/assets/tests/views/admin/PluginsManagement.test.tsx b/resources/assets/tests/views/admin/PluginsManagement.test.tsx new file mode 100644 index 00000000..bebef074 --- /dev/null +++ b/resources/assets/tests/views/admin/PluginsManagement.test.tsx @@ -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() + 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() + 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() + 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() + 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('
  • abc
  • '), + }) + }) +}) + +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() + 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() + 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() + 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() + 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() + 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() + }) +}) diff --git a/resources/assets/tests/webpack.d.ts b/resources/assets/tests/webpack.d.ts index 1a135a4b..8c582452 100644 --- a/resources/assets/tests/webpack.d.ts +++ b/resources/assets/tests/webpack.d.ts @@ -5,9 +5,5 @@ declare module '*.vue' { } declare module '*.styl' { - export default {} -} - -declare module '*.yml' { - export default {} + export default {} as Record } diff --git a/tsconfig.json b/tsconfig.json index a098af7f..6ee8f133 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ] } diff --git a/webpack.config.js b/webpack.config.js index b80ea95f..48472f4c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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({})], diff --git a/yarn.lock b/yarn.lock index 4a14fb23..6cbd0f92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 4c066ac38aa922996235579ddd4174e8f6ca799f Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 21 Jan 2020 11:07:20 +0800 Subject: [PATCH 02/58] make user report page be static --- app/Http/Controllers/ReportController.php | 4 ++- resources/assets/src/scripts/route.ts | 5 --- resources/views/user/report.twig | 32 +++++++++++++++++++ routes/web.php | 5 +-- .../ControllersTest/ReportControllerTest.php | 9 ++---- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 34dc4722..da4337ee 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -56,9 +56,11 @@ class ReportController extends Controller public function track() { - return Report::where('reporter', auth()->id()) + $reports = Report::where('reporter', auth()->id()) ->orderBy('report_at', 'desc') ->get(); + + return view('user.report', ['reports' => $reports]); } public function manage(Request $request) diff --git a/resources/assets/src/scripts/route.ts b/resources/assets/src/scripts/route.ts index f2dfad30..5d253166 100644 --- a/resources/assets/src/scripts/route.ts +++ b/resources/assets/src/scripts/route.ts @@ -28,11 +28,6 @@ export default [ component: () => import('../views/user/Bind.vue'), el: 'form', }, - { - path: 'user/reports', - component: () => import('../views/user/Report.vue'), - el: '.content > .container-fluid', - }, { path: 'user/profile', module: [ diff --git a/resources/views/user/report.twig b/resources/views/user/report.twig index c6906d63..16c46736 100644 --- a/resources/views/user/report.twig +++ b/resources/views/user/report.twig @@ -1,3 +1,35 @@ {% extends 'user.base' %} {% block title %}{{ trans('general.my-reports') }}{% endblock %} + +{% block content %} +
    +
    + + + + + + + + + + + {% for report in reports %} + + + + + + + {% endfor %} + +
    {{ trans('front-end.report.tid') }}{{ trans('front-end.report.reason') }}{{ trans('front-end.report.status-title') }}{{ trans('front-end.report.time') }}
    + {{ report.tid }}  + + + + {{ report.reason }}{{ trans('front-end.report.status')[report.status] }}{{ report.report_at }}
    +
    +
    +{% endblock %} diff --git a/routes/web.php b/routes/web.php index b2cb3562..853ca1b1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -57,10 +57,7 @@ Route::prefix('user') Route::get('score-info', 'UserController@scoreInfo')->name('score'); Route::post('sign', 'UserController@sign')->name('sign'); - Route::prefix('reports')->group(function () { - Route::view('', 'user.report'); - Route::get('list', 'ReportController@track'); - }); + Route::get('reports', 'ReportController@track'); Route::prefix('profile')->group(function () { Route::get('', 'UserController@profile')->name('profile'); diff --git a/tests/HttpTest/ControllersTest/ReportControllerTest.php b/tests/HttpTest/ControllersTest/ReportControllerTest.php index 84f87d00..7e3671b5 100644 --- a/tests/HttpTest/ControllersTest/ReportControllerTest.php +++ b/tests/HttpTest/ControllersTest/ReportControllerTest.php @@ -113,12 +113,9 @@ class ReportControllerTest extends TestCase $report->save(); $this->actingAs($user) - ->getJson('/user/reports/list') - ->assertJson([[ - 'tid' => 1, - 'reason' => 'test', - 'status' => Report::PENDING, - ]]); + ->getJson('/user/reports') + ->assertSee('test') + ->assertSee(trans('front-end.report.status')[0]); } public function testManage() From be0f74ad6461105ffeb6c40471449e1770e43494 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 21 Jan 2020 11:08:52 +0800 Subject: [PATCH 03/58] remove unused code --- resources/assets/src/views/admin/Plugins.vue | 163 ------------------ resources/assets/src/views/user/Report.vue | 82 --------- .../assets/tests/views/admin/Plugins.test.ts | 138 --------------- .../assets/tests/views/user/Report.test.ts | 17 -- 4 files changed, 400 deletions(-) delete mode 100644 resources/assets/src/views/admin/Plugins.vue delete mode 100644 resources/assets/src/views/user/Report.vue delete mode 100644 resources/assets/tests/views/admin/Plugins.test.ts delete mode 100644 resources/assets/tests/views/user/Report.test.ts diff --git a/resources/assets/src/views/admin/Plugins.vue b/resources/assets/src/views/admin/Plugins.vue deleted file mode 100644 index 3ef0fdc9..00000000 --- a/resources/assets/src/views/admin/Plugins.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - diff --git a/resources/assets/src/views/user/Report.vue b/resources/assets/src/views/user/Report.vue deleted file mode 100644 index ee345e88..00000000 --- a/resources/assets/src/views/user/Report.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/resources/assets/tests/views/admin/Plugins.test.ts b/resources/assets/tests/views/admin/Plugins.test.ts deleted file mode 100644 index b1ff5e2a..00000000 --- a/resources/assets/tests/views/admin/Plugins.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import { flushPromises } from '../../utils' -import { showModal, toast } from '@/scripts/notify' -import Plugins from '@/views/admin/Plugins.vue' - -jest.mock('@/scripts/notify') - -test('render config button', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - name: 'a', icon: {}, enabled: true, config: true, - }, - { - name: 'b', icon: {}, enabled: false, config: true, - }, - { - name: 'c', icon: {}, enabled: false, config: false, - }, - ]) - const wrapper = mount(Plugins) - await flushPromises() - - expect(wrapper.find('.info-box:nth-child(1) .fa-cog').exists()).toBeTrue() - expect(wrapper.find('.info-box:nth-child(2) .fa-cog').exists()).toBeFalse() - expect(wrapper.find('.info-box:nth-child(3) .fa-cog').exists()).toBeFalse() -}) - -test('enable plugin', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - name: 'a', icon: {}, enabled: false, - }, - ]) - Vue.prototype.$http.post - .mockResolvedValueOnce({ - code: 1, message: '1', data: { reason: ['abc'] }, - }) - .mockResolvedValue({ code: 0, message: '0' }) - const wrapper = mount(Plugins) - await flushPromises() - const checkbox = wrapper.find('input[type=checkbox]') - - checkbox.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/plugins/manage', - { action: 'enable', name: 'a' }, - ) - expect(showModal).toBeCalledWith({ - mode: 'alert', - dangerousHTML: expect.stringContaining('
  • abc
  • '), - }) - - checkbox.trigger('click') - await flushPromises() - expect(toast.success).toBeCalled() -}) - -test('disable plugin', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - name: 'a', icon: {}, enabled: true, - }, - ]) - Vue.prototype.$http.post - .mockResolvedValueOnce({ code: 1, message: '1' }) - .mockResolvedValue({ code: 0, message: '0' }) - const wrapper = mount(Plugins) - await flushPromises() - const checkbox = wrapper.find('input[type="checkbox"]') - - checkbox.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/plugins/manage', - { action: 'disable', name: 'a' }, - ) - checkbox.trigger('click') - await flushPromises() - expect(toast.success).toBeCalledWith('0') - expect(checkbox.attributes('checked')).toBeFalsy() -}) - -test('delete plugin', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - name: 'a', - title: 'My Plugin', - icon: {}, - }, - ]) - Vue.prototype.$http.post - .mockResolvedValueOnce({ code: 1, message: '1' }) - .mockResolvedValue({ code: 0, message: '0' }) - showModal - .mockRejectedValueOnce(null) - .mockResolvedValue({ value: '' }) - const wrapper = mount(Plugins) - await flushPromises() - const button = wrapper.find('.plugin-actions a') - - button.trigger('click') - await flushPromises() - expect(showModal).toBeCalledWith({ - title: 'My Plugin', - text: 'admin.confirmDeletion', - okButtonType: 'danger', - }) - expect(Vue.prototype.$http.post).not.toBeCalled() - - button.trigger('click') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/admin/plugins/manage', - { action: 'delete', name: 'a' }, - ) - expect(toast.error).toBeCalledWith('1') - - button.trigger('click') - await flushPromises() - expect(wrapper.text()).not.toContain('My Plugin') -}) - -test('readme link', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - name: 'a', - readme: true, - icon: {}, - }, - ]) - const wrapper = mount(Plugins) - await flushPromises() - - const link = wrapper.find('.plugin-actions > a:nth-child(1)') - expect(link.attributes('href')).toBe('/admin/plugins/readme/a') -}) diff --git a/resources/assets/tests/views/user/Report.test.ts b/resources/assets/tests/views/user/Report.test.ts deleted file mode 100644 index a12dedf7..00000000 --- a/resources/assets/tests/views/user/Report.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import { flushPromises } from '../../utils' -import Report from '@/views/user/Report.vue' - -test('basic render', async () => { - Vue.prototype.$http.get.mockResolvedValue([ - { - id: 1, tid: 1, reason: 'abc', status: 1, - }, - ]) - const wrapper = mount(Report) - await flushPromises() - - expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1') - expect(wrapper.text()).toContain('report.status.1') -}) From 11e927cc6174c8cbf3bfbd242b9f1651f8be2eae Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 23 Jan 2020 09:51:50 +0800 Subject: [PATCH 04/58] add "jest-dom" --- package.json | 1 + resources/assets/tests/setup.ts | 1 + .../views/admin/PluginsManagement.test.tsx | 2 + yarn.lock | 70 +++++++++++++++++-- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b0ab853f..690fc6f2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@babel/plugin-transform-runtime": "^7.8.3", "@babel/preset-env": "^7.8.2", "@gplane/tsconfig": "^1.0.0", + "@testing-library/jest-dom": "^5.0.2", "@testing-library/react": "^9.4.0", "@types/bootstrap": "^4.3.1", "@types/echarts": "^4.4.2", diff --git a/resources/assets/tests/setup.ts b/resources/assets/tests/setup.ts index db63f5e4..52f16f1e 100644 --- a/resources/assets/tests/setup.ts +++ b/resources/assets/tests/setup.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import 'jest-extended' +import '@testing-library/jest-dom' import Vue from 'vue' window.blessing = { diff --git a/resources/assets/tests/views/admin/PluginsManagement.test.tsx b/resources/assets/tests/views/admin/PluginsManagement.test.tsx index bebef074..551aab62 100644 --- a/resources/assets/tests/views/admin/PluginsManagement.test.tsx +++ b/resources/assets/tests/views/admin/PluginsManagement.test.tsx @@ -70,6 +70,7 @@ describe('enable plugin', () => { name: 'a', }) expect(toast.success).toBeCalled() + expect(getByTitle(trans('admin.disablePlugin'))).toBeChecked() }) it('failed', async () => { @@ -121,6 +122,7 @@ describe('disable plugin', () => { name: 'a', }) expect(toast.success).toBeCalledWith('0') + expect(getByTitle(trans('admin.enablePlugin'))).not.toBeChecked() }) it('failed', async () => { diff --git a/yarn.lock b/yarn.lock index 6cbd0f92..b912c3be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -828,7 +828,7 @@ levenary "^1.1.0" semver "^5.5.0" -"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6": +"@babel/runtime@^7.5.1", "@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== @@ -1170,6 +1170,22 @@ pretty-format "^24.9.0" wait-for-expect "^3.0.0" +"@testing-library/jest-dom@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.0.2.tgz#dbd06cc9a392347327597dfd633b9d7623da5c80" + integrity sha512-Pl3si4eH88+5wlVz6NjecLfeO7PjTbbgWKwVZXUtBfrGuJLjGdUUo+O92XRu/59wft8HAmwvXHeMWpqWTg2Uwg== + dependencies: + "@babel/runtime" "^7.5.1" + "@types/testing-library__jest-dom" "^5.0.0" + chalk "^2.4.1" + css "^2.2.3" + css.escape "^1.5.1" + jest-diff "^24.0.0" + jest-matcher-utils "^24.0.0" + lodash "^4.17.11" + pretty-format "^24.0.0" + redent "^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" @@ -1281,6 +1297,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.0.tgz#78c6991cd1734cf0d390be24875e310bb0a9fb74" + integrity sha512-dXvuABY9nM1xgsXlOtLQXJKdacxZJd7AtvLsKZ/0b57ruMXDKCOXAC/M75GbllQX6o1pcZ5hAG4JzYy7Z/wM2w== + dependencies: + jest-diff "^24.3.0" + "@types/jest@^24.0.25": version "24.0.25" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.25.tgz#2aba377824ce040114aa906ad2cac2c85351360f" @@ -1369,6 +1392,13 @@ dependencies: pretty-format "^24.3.0" +"@types/testing-library__jest-dom@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.0.tgz#db048883864126ae73ec043587238a06c3aaf837" + integrity sha512-Pm+2mvqs9sND247oMVJbu3QmPUqMzBuJbY5uHHYQdl9MXq5hwWkTbesw1sPIky52Y0sHksWAZnVvGLbdyFSyrA== + dependencies: + "@types/jest" "*" + "@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" @@ -3151,7 +3181,12 @@ css-what@^2.1.2: resolved "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== -css@^2.0.0: +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + +css@^2.0.0, css@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== @@ -5291,6 +5326,11 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -5799,7 +5839,7 @@ jest-config@^24.9.0: pretty-format "^24.9.0" realpath-native "^1.1.0" -jest-diff@^24.3.0, jest-diff@^24.9.0: +jest-diff@^24.0.0, jest-diff@^24.3.0, jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== @@ -5925,7 +5965,7 @@ jest-matcher-utils@^22.0.0: jest-get-type "^22.4.3" pretty-format "^22.4.3" -jest-matcher-utils@^24.9.0: +jest-matcher-utils@^24.0.0, jest-matcher-utils@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== @@ -6754,6 +6794,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +min-indent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" + integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= + 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" @@ -8066,7 +8111,7 @@ pretty-format@^22.4.3: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-format@^24.3.0, pretty-format@^24.9.0: +pretty-format@^24.0.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== @@ -8394,6 +8439,14 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -9435,6 +9488,13 @@ strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" From 7c144b3e14434558d13711de505d35b1878d8350 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 23 Jan 2020 22:52:09 +0800 Subject: [PATCH 05/58] rewrite "Modal" component in React --- package.json | 2 +- resources/assets/src/components/Modal.tsx | 164 +++++++++++ resources/assets/src/components/ModalBody.tsx | 52 ++++ .../assets/src/components/ModalFooter.tsx | 46 ++++ .../assets/src/components/ModalHeader.tsx | 25 ++ resources/assets/src/scripts/modal.ts | 51 ---- resources/assets/src/scripts/modal.tsx | 32 +++ resources/assets/src/scripts/validators.ts | 2 +- resources/assets/src/shims.d.ts | 2 +- .../assets/tests/components/Modal.test.tsx | 255 ++++++++++++++++++ resources/assets/tests/scripts/modal.test.ts | 6 +- resources/assets/tests/ts-shims/notify.ts | 2 +- 12 files changed, 583 insertions(+), 56 deletions(-) create mode 100644 resources/assets/src/components/Modal.tsx create mode 100644 resources/assets/src/components/ModalBody.tsx create mode 100644 resources/assets/src/components/ModalFooter.tsx create mode 100644 resources/assets/src/components/ModalHeader.tsx delete mode 100644 resources/assets/src/scripts/modal.ts create mode 100644 resources/assets/src/scripts/modal.tsx create mode 100644 resources/assets/tests/components/Modal.test.tsx diff --git a/package.json b/package.json index 690fc6f2..a49b44a3 100644 --- a/package.json +++ b/package.json @@ -194,9 +194,9 @@ }, "moduleFileExtensions": [ "js", - "vue", "ts", "tsx", + "vue", "json", "node" ], diff --git a/resources/assets/src/components/Modal.tsx b/resources/assets/src/components/Modal.tsx new file mode 100644 index 00000000..21678959 --- /dev/null +++ b/resources/assets/src/components/Modal.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useRef } from 'react' +import $ from 'jquery' +import 'bootstrap' +import { trans } from '../scripts/i18n' +import ModalHeader from './ModalHeader' +import ModalBody from './ModalBody' +import ModalFooter from './ModalFooter' + +export type ModalOptions = { + mode?: 'alert' | 'confirm' | 'prompt' + show?: boolean + title?: string + text?: string + dangerousHTML?: string + input?: string + placeholder?: string + inputType?: string + validator?(value: any): string | boolean | undefined + type?: string + showHeader?: boolean + center?: boolean + okButtonText?: string + okButtonType?: string + cancelButtonText?: string + cancelButtonType?: string + flexFooter?: boolean +} + +type Props = { + id?: string + children?: React.ReactNode + footer?: React.ReactNode + onConfirm?(payload: { value: string }): void + onDismiss?(): void +} + +export type ModalResult = { + value: string +} + +const Modal = React.forwardRef( + (props, forwardedRef) => { + const [hidden, setHidden] = useState(false) + const [value, setValue] = useState(props.input!) + const [valid, setValid] = useState(true) + const [validatorMessage, setValidatorMessage] = useState('') + const ref = (forwardedRef ?? + useRef(null)) as React.RefObject + + const handleInputChange = (event: React.ChangeEvent) => { + setValue(event.target.value) + } + + const confirm = () => { + const { validator } = props + if (typeof validator === 'function') { + const result = validator(value) + if (typeof result === 'string') { + setValidatorMessage(result) + setValid(false) + return + } + } + + setHidden(true) + props.onConfirm?.({ value }) + $(ref.current!).modal('hide') + } + + const dismiss = () => { + setHidden(true) + props.onDismiss?.() + } + + useEffect(() => { + const onHide = () => { + /* istanbul ignore else */ + if (!hidden) { + dismiss() + } + } + const onHidden = () => setHidden(false) + + const el = $(ref.current!) + el.on('hide.bs.modal', onHide).on('hidden.bs.modal', onHidden) + + return () => { + el.off('hide.bs.modal', onHide).off('hidden.bs.modal', onHidden) + } + }, []) + + return ( + + ) + }, +) + +Modal.displayName = 'Modal' + +Modal.defaultProps = { + mode: 'confirm', + title: trans('general.tip'), + text: '', + input: '', + placeholder: '', + inputType: 'text', + type: 'default', + showHeader: true, + center: false, + okButtonText: trans('general.confirm'), + okButtonType: 'primary', + cancelButtonText: trans('general.cancel'), + cancelButtonType: 'secondary', + flexFooter: false, +} + +export default Modal diff --git a/resources/assets/src/components/ModalBody.tsx b/resources/assets/src/components/ModalBody.tsx new file mode 100644 index 00000000..8c97a164 --- /dev/null +++ b/resources/assets/src/components/ModalBody.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +interface Props { + text?: string + dangerousHTML?: string + showInput: boolean + inputType?: string + value?: string + onChange?: React.ChangeEventHandler + placeholder?: string + invalid?: boolean + validatorMessage?: string +} + +const ModalBody: React.FC = props => { + const main = (() => { + if (props.children) { + return props.children + } else if (props.text) { + return props.text.split(/\r?\n/).map((line, i) =>

    {line}

    ) + } else if (props.dangerousHTML) { + return
    + } + })() + + return ( +
    + {main} + {props.showInput && ( + <> +
    + +
    + {props.invalid && ( +
    + + {props.validatorMessage} +
    + )} + + )} +
    + ) +} + +export default ModalBody diff --git a/resources/assets/src/components/ModalFooter.tsx b/resources/assets/src/components/ModalFooter.tsx new file mode 100644 index 00000000..dc26b81f --- /dev/null +++ b/resources/assets/src/components/ModalFooter.tsx @@ -0,0 +1,46 @@ +import React from 'react' + +interface Props { + showCancelButton: boolean + flexFooter?: boolean + okButtonText?: string + okButtonType?: string + cancelButtonText?: string + cancelButtonType?: string + onConfirm?(): void + onDismiss?(): void +} + +const ModalFooter: React.FC = props => { + const classes = ['modal-footer'] + if (props.flexFooter) { + classes.push('d-flex', 'justify-content-between') + } + const footerClass = classes.join(' ') + + return props.children ? ( +
    {props.children}
    + ) : ( +
    + {props.showCancelButton && ( + + )} + +
    + ) +} + +export default ModalFooter diff --git a/resources/assets/src/components/ModalHeader.tsx b/resources/assets/src/components/ModalHeader.tsx new file mode 100644 index 00000000..b363639f --- /dev/null +++ b/resources/assets/src/components/ModalHeader.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +interface Props { + show?: boolean + title?: string + onDismiss?(): void +} + +const ModalHeader: React.FC = props => + props.show ? ( +
    +
    {props.title}
    + +
    + ) : null + +export default ModalHeader diff --git a/resources/assets/src/scripts/modal.ts b/resources/assets/src/scripts/modal.ts deleted file mode 100644 index c74e94c8..00000000 --- a/resources/assets/src/scripts/modal.ts +++ /dev/null @@ -1,51 +0,0 @@ -import $ from 'jquery' -import 'bootstrap' -import Vue from 'vue' -import Modal from '../components/Modal.vue' - -export interface ModalOptions { - mode?: 'alert' | 'confirm' | 'prompt' - title?: string - text?: string - dangerousHTML?: string - input?: string - placeholder?: string - inputType?: string - validator?(value: any): string | boolean | void - type?: string - showHeader?: boolean - center?: boolean - okButtonText?: string - okButtonType?: string - cancelButtonText?: string - cancelButtonType?: string - flexFooter?: boolean -} - -export interface ModalResult { - value: string -} - -export function showModal(options: ModalOptions = {}): Promise { - return new Promise((resolve, reject) => { - const container = document.createElement('div') - document.body.appendChild(container) - - const instance = new Vue({ - render: h => h(Modal, { - props: Object.assign({ center: true }, options), - on: { - confirm: resolve, - dismiss: reject, - }, - }), - }).$mount(container) - - $(instance.$el) - .modal('show') - .on('hidden.bs.modal', () => { - instance.$el.remove() - instance.$destroy() - }) - }) -} diff --git a/resources/assets/src/scripts/modal.tsx b/resources/assets/src/scripts/modal.tsx new file mode 100644 index 00000000..b5b1a985 --- /dev/null +++ b/resources/assets/src/scripts/modal.tsx @@ -0,0 +1,32 @@ +import $ from 'jquery' +import React from 'react' +import ReactDOM from 'react-dom' +import Modal, { ModalOptions, ModalResult } from '../components/Modal' + +export function showModal(options: ModalOptions = {}): Promise { + return new Promise((resolve, reject) => { + const container = document.createElement('div') + document.body.appendChild(container) + + const ref = React.createRef() + ReactDOM.render( + , + container, + ) + + $(ref.current!) + .modal('show') + .on('hidden.bs.modal', () => { + setTimeout(() => { + ReactDOM.unmountComponentAtNode(container) + container.remove() + }, 0) + }) + }) +} diff --git a/resources/assets/src/scripts/validators.ts b/resources/assets/src/scripts/validators.ts index 06ea3805..17ca43bd 100644 --- a/resources/assets/src/scripts/validators.ts +++ b/resources/assets/src/scripts/validators.ts @@ -1,5 +1,5 @@ export function truthy(message: string) { - return (value?: unknown): string | void => { + return (value?: unknown): string | undefined => { if (!value) { return message } diff --git a/resources/assets/src/shims.d.ts b/resources/assets/src/shims.d.ts index 707fd334..c9e3758d 100644 --- a/resources/assets/src/shims.d.ts +++ b/resources/assets/src/shims.d.ts @@ -1,6 +1,6 @@ import Vue from 'vue' import JQuery from 'jquery' -import { ModalOptions, ModalResult } from './scripts/modal' +import { ModalOptions, ModalResult } from './components/Modal' import { Toast } from './scripts/toast' declare global { diff --git a/resources/assets/tests/components/Modal.test.tsx b/resources/assets/tests/components/Modal.test.tsx new file mode 100644 index 00000000..fcddd081 --- /dev/null +++ b/resources/assets/tests/components/Modal.test.tsx @@ -0,0 +1,255 @@ +import React from 'react' +import { render, fireEvent, act } from '@testing-library/react' +import { trans } from '@/scripts/i18n' +import $ from 'jquery' +import Modal from '@/components/Modal' + +test('hidden by default', () => { + const { queryByRole } = render() + expect(queryByRole('dialog')).toBeNull() +}) + +test('receive id', () => { + const { getByRole } = render() + expect(getByRole('dialog')).toHaveAttribute('id', 'kumiko') +}) + +test('centered dialog', () => { + const { getByRole } = render() + expect(getByRole('document')).toHaveClass('modal-dialog-centered') +}) + +test('background color', () => { + const { container } = render() + expect(container.querySelector('.modal-content')).toHaveClass('bg-primary') +}) + +test('forward ref', () => { + const ref = React.createRef() + render() + expect(ref.current).not.toBeNull() +}) + +test('jQuery events', () => { + const ref = React.createRef() + render() + act(() => { + $(ref.current!) + .trigger('hide.bs.modal') + .trigger('hidden.bs.modal') + }) +}) + +describe('modal header', () => { + it('modal title', () => { + const { queryByText } = render() + expect(queryByText('Tips')).toBeInTheDocument() + }) + + it('hide modal header', () => { + const { queryByText } = render( + , + ) + expect(queryByText('Tips')).not.toBeInTheDocument() + }) +}) + +describe('modal body', () => { + it('custom children', () => { + const { queryByText } = render(body) + expect(queryByText('body')).toBeInTheDocument() + }) + + it('text with linebreaks', () => { + const { queryByText } = render() + expect(queryByText('L1')).toBeInTheDocument() + expect(queryByText('L2')).toBeInTheDocument() + }) + + it('dangerous HTML', () => { + const { getByText } = render( + , + ) + expect(getByText('ab')).toHaveClass('h1') + }) + + describe('input control', () => { + it('set default value', () => { + const { queryByDisplayValue } = render( + , + ) + expect(queryByDisplayValue('val')).toBeInTheDocument() + }) + + it('placeholder', () => { + const { queryByPlaceholderText } = render( + , + ) + expect(queryByPlaceholderText('hint')).toBeInTheDocument() + }) + + it('input control type', () => { + const { getByPlaceholderText } = render( + , + ) + expect(getByPlaceholderText('password')).toHaveAttribute( + 'type', + 'password', + ) + }) + }) +}) + +describe('modal footer', () => { + it('custom footer content', () => { + const { queryByText } = render(footer
    } show />) + expect(queryByText('footer')).toBeInTheDocument() + expect(queryByText(trans('general.confirm'))).not.toBeInTheDocument() + }) + + it('flex footer', () => { + const { getByText } = render() + expect(getByText('footer')).toHaveClass('d-flex', 'justify-content-between') + }) + + it('custom ok button', () => { + const { getByText } = render( + , + ) + expect(getByText('kumiko')).toHaveClass('btn-primary') + }) + + it('custom cancel button', () => { + const { getByText } = render( + , + ) + expect(getByText('reina')).toHaveClass('btn-success') + }) +}) + +describe('"alert" mode', () => { + it('buttons', () => { + const resolve = jest.fn() + const { getByText, queryByText } = render( + , + ) + fireEvent.click(getByText(trans('general.confirm'))) + expect(resolve).toBeCalledWith({ value: '' }) + expect(queryByText(trans('general.cancel'))).toBeNull() + }) + + it('confirm callback is optional', () => { + const { getByText } = render() + fireEvent.click(getByText(trans('general.confirm'))) + }) +}) + +describe('"confirm" mode', () => { + it('default mode is "confirm"', () => { + const { queryByText } = render() + expect(queryByText(trans('general.confirm'))).toBeInTheDocument() + expect(queryByText(trans('general.cancel'))).toBeInTheDocument() + }) + + it('"confirm" button', () => { + const resolve = jest.fn() + const reject = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(trans('general.confirm'))) + expect(resolve).toBeCalledWith({ value: '' }) + expect(reject).not.toBeCalled() + }) + + it('"cancel" button', () => { + const resolve = jest.fn() + const reject = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(trans('general.cancel'))) + expect(resolve).not.toBeCalled() + expect(reject).toBeCalled() + }) +}) + +describe('"prompt" mode', () => { + it('retrieve input value', () => { + const resolve = jest.fn() + const reject = jest.fn() + const { getByPlaceholderText, getByText } = render( + , + ) + fireEvent.change(getByPlaceholderText('hint'), { + target: { value: 'my' }, + }) + fireEvent.click(getByText(trans('general.confirm'))) + expect(resolve).toBeCalledWith({ value: 'my' }) + expect(reject).not.toBeCalled() + }) + + it('cancel dialog', () => { + const resolve = jest.fn() + const reject = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(trans('general.cancel'))) + expect(resolve).not.toBeCalled() + expect(reject).toBeCalled() + }) + + it('validate input', () => { + const resolve = jest.fn() + const reject = jest.fn() + const validator = jest.fn().mockReturnValue(true) + const { getByText } = render( + , + ) + fireEvent.click(getByText(trans('general.confirm'))) + expect(resolve).toBeCalledWith({ value: 'val' }) + expect(reject).not.toBeCalled() + }) + + it('report validator message', () => { + const message = 'Invalid input.' + const resolve = jest.fn() + const reject = jest.fn() + const validator = jest.fn().mockReturnValue(message) + const { getByText, queryByText } = render( + , + ) + expect(queryByText(message)).not.toBeInTheDocument() + + fireEvent.click(getByText(trans('general.confirm'))) + expect(queryByText(message)).toBeInTheDocument() + expect(resolve).not.toBeCalled() + expect(reject).not.toBeCalled() + }) +}) diff --git a/resources/assets/tests/scripts/modal.test.ts b/resources/assets/tests/scripts/modal.test.ts index e47e529c..203bf39c 100644 --- a/resources/assets/tests/scripts/modal.test.ts +++ b/resources/assets/tests/scripts/modal.test.ts @@ -1,4 +1,5 @@ import $ from 'jquery' +import { act } from 'react-dom/test-utils' import { showModal } from '@/scripts/modal' test('show modal', async () => { @@ -11,6 +12,9 @@ test('show modal', async () => { const { value } = await showModal() expect(value).toBe('') - $('.modal').trigger('hidden.bs.modal') + act(() => { + $('.modal').trigger('hidden.bs.modal') + jest.runAllTimers() + }) expect(document.querySelector('.modal')).toBeNull() }) diff --git a/resources/assets/tests/ts-shims/notify.ts b/resources/assets/tests/ts-shims/notify.ts index 7b2b8f46..ce5267bd 100644 --- a/resources/assets/tests/ts-shims/notify.ts +++ b/resources/assets/tests/ts-shims/notify.ts @@ -1,4 +1,4 @@ -import { ModalOptions, ModalResult } from '../../src/scripts/modal' +import { ModalOptions, ModalResult } from '../../src/components/Modal' import { Toast } from '../../src/scripts/toast' export const showModal = {} as jest.Mock, [ModalOptions | void]> From 999167e11e2b36fa0cf3c30f63fd0d0ad8ffa917 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 27 Jan 2020 11:50:29 +0800 Subject: [PATCH 06/58] fix hooks deps --- resources/assets/src/components/Modal.tsx | 3 +-- .../assets/src/views/admin/PluginsManagement/InfoBox.tsx | 4 ++-- resources/assets/tests/components/Modal.test.tsx | 9 ++++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/resources/assets/src/components/Modal.tsx b/resources/assets/src/components/Modal.tsx index 21678959..5fb8fa4f 100644 --- a/resources/assets/src/components/Modal.tsx +++ b/resources/assets/src/components/Modal.tsx @@ -74,7 +74,6 @@ const Modal = React.forwardRef( useEffect(() => { const onHide = () => { - /* istanbul ignore else */ if (!hidden) { dismiss() } @@ -87,7 +86,7 @@ const Modal = React.forwardRef( return () => { el.off('hide.bs.modal', onHide).off('hidden.bs.modal', onHidden) } - }, []) + }, [hidden, props.onDismiss]) return (
    = props => { props.onDisable(plugin) } }, - [], + [plugin], ) const handleDelete = useCallback(() => { props.onDelete(plugin) - }, []) + }, [plugin]) return (
    diff --git a/resources/assets/tests/components/Modal.test.tsx b/resources/assets/tests/components/Modal.test.tsx index fcddd081..41a9725c 100644 --- a/resources/assets/tests/components/Modal.test.tsx +++ b/resources/assets/tests/components/Modal.test.tsx @@ -32,7 +32,14 @@ test('forward ref', () => { test('jQuery events', () => { const ref = React.createRef() - render() + const { getByText } = render() + act(() => { + $(ref.current!) + .trigger('hide.bs.modal') + .trigger('hidden.bs.modal') + }) + + fireEvent.click(getByText(trans('general.cancel'))) act(() => { $(ref.current!) .trigger('hide.bs.modal') From 225985834a4280556622e285107f316e24853715 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 27 Jan 2020 11:56:32 +0800 Subject: [PATCH 07/58] fix tsconfig --- resources/assets/tests/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/assets/tests/tsconfig.json b/resources/assets/tests/tsconfig.json index 5da8e1c0..27f92e5f 100644 --- a/resources/assets/tests/tsconfig.json +++ b/resources/assets/tests/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "baseUrl": "..", + "noEmit": true, "paths": { "@/scripts/*": [ "./tests/ts-shims/*", From 60e883c5e745a687d59f769e68ed36100b23b3fb Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 27 Jan 2020 15:47:24 +0800 Subject: [PATCH 08/58] remove cache-loader for .ts and .tsx --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 48472f4c..bc34d8ce 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,7 +39,7 @@ const config = { }, { test: /\.tsx?$/, - use: ['cache-loader', 'ts-loader'], + use: 'ts-loader', }, { test: /\.vue$/, From d62db082e7e5807ff8bc956137d894e9038450a4 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 27 Jan 2020 15:49:09 +0800 Subject: [PATCH 09/58] add scss --- package.json | 4 +- .../admin/PluginsManagement/InfoBox.scss | 43 ++++++++ .../admin/PluginsManagement/InfoBox.styl | 31 ------ .../views/admin/PluginsManagement/InfoBox.tsx | 2 +- resources/assets/src/webpack.d.ts | 4 + resources/assets/tests/webpack.d.ts | 4 + webpack.config.js | 4 +- yarn.lock | 104 +++++++++++++++++- 8 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 resources/assets/src/views/admin/PluginsManagement/InfoBox.scss delete mode 100644 resources/assets/src/views/admin/PluginsManagement/InfoBox.styl diff --git a/package.json b/package.json index a49b44a3..dc645ae8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "mini-css-extract-plugin": "^0.9.0", "optimize-css-assets-webpack-plugin": "^5.0.3", "postcss-loader": "^3.0.0", + "sass": "^1.25.0", + "sass-loader": "^8.0.2", "style-loader": "^1.1.2", "stylus": "^0.54.7", "stylus-loader": "^3.0.2", @@ -202,7 +204,7 @@ ], "moduleNameMapper": { "^@/(.*)$": "/resources/assets/src/$1", - "\\.(css|styl)$": "/resources/assets/tests/__mocks__/style.ts", + "\\.(s?css|styl)$": "/resources/assets/tests/__mocks__/style.ts", "\\.(png|jpg)$": "/resources/assets/tests/__mocks__/file.ts" }, "setupFilesAfterEnv": [ diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.scss b/resources/assets/src/views/admin/PluginsManagement/InfoBox.scss new file mode 100644 index 00000000..ba450302 --- /dev/null +++ b/resources/assets/src/views/admin/PluginsManagement/InfoBox.scss @@ -0,0 +1,43 @@ +.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 0.5rem 1rem rgba(#000, 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; +} diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl b/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl deleted file mode 100644 index 5d11ef8d..00000000 --- a/resources/assets/src/views/admin/PluginsManagement/InfoBox.styl +++ /dev/null @@ -1,31 +0,0 @@ -.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 diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx index e02d6b73..54afeaf9 100644 --- a/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx +++ b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react' import { trans } from '../../../scripts/i18n' import { Plugin } from './types' -import styles from './InfoBox.styl' +import styles from './InfoBox.scss' interface Props { plugin: Plugin diff --git a/resources/assets/src/webpack.d.ts b/resources/assets/src/webpack.d.ts index 8c582452..e89f05da 100644 --- a/resources/assets/src/webpack.d.ts +++ b/resources/assets/src/webpack.d.ts @@ -7,3 +7,7 @@ declare module '*.vue' { declare module '*.styl' { export default {} as Record } + +declare module '*.scss' { + export default {} as Record +} diff --git a/resources/assets/tests/webpack.d.ts b/resources/assets/tests/webpack.d.ts index 8c582452..e89f05da 100644 --- a/resources/assets/tests/webpack.d.ts +++ b/resources/assets/tests/webpack.d.ts @@ -7,3 +7,7 @@ declare module '*.vue' { declare module '*.styl' { export default {} as Record } + +declare module '*.scss' { + export default {} as Record +} diff --git a/webpack.config.js b/webpack.config.js index bc34d8ce..5aa7c3fb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -55,7 +55,7 @@ const config = { ], }, { - test: /views.*\.styl$/, + test: /\.scss$/, use: [ 'style-loader', { @@ -70,7 +70,7 @@ const config = { }, }, 'postcss-loader', - 'stylus-loader', + 'sass-loader', ], }, { diff --git a/yarn.lock b/yarn.lock index b912c3be..5af389e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1930,6 +1930,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2230,6 +2238,11 @@ binary-extensions@^1.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bluebird@^3.1.1, bluebird@^3.5.5: version "3.5.5" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" @@ -2325,7 +2338,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2682,6 +2695,21 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" +"chokidar@>=2.0.0 <4.0.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + chokidar@^2.0.2, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2770,6 +2798,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + clone@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -4795,6 +4832,11 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" +fsevents@~2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" + integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4871,6 +4913,13 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-parent@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.4" resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -5491,6 +5540,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5607,7 +5663,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -7114,7 +7170,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7634,7 +7690,7 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.5: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== @@ -8432,6 +8488,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== + dependencies: + picomatch "^2.0.7" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -8792,6 +8855,24 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" +sass-loader@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" + integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== + dependencies: + clone-deep "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" + semver "^6.3.0" + +sass@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.25.0.tgz#f8bd7dfbb39d6b0305e27704a8ebe637820693f3" + integrity sha512-uQMjye0Y70SEDGO56n0j91tauqS9E1BmpKHtiYNQScXDHeaE9uHwNEqQNFf4Bes/3DHMNinB6u79JsG10XWNyw== + dependencies: + chokidar ">=2.0.0 <4.0.0" + sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -8838,6 +8919,14 @@ schema-utils@^2.5.0, schema-utils@^2.6.0: ajv "^6.10.2" ajv-keywords "^3.4.1" +schema-utils@^2.6.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" + integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== + dependencies: + ajv "^6.10.2" + ajv-keywords "^3.4.1" + scope-analyzer@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/scope-analyzer/-/scope-analyzer-2.0.5.tgz#72c9c6770c3e66984f84c7d3c7045998a1a7db8a" @@ -8987,6 +9076,13 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + shallow-copy@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" From 6b714ad7b3d4d50f8caeb1994f453b92bc0454cf Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 27 Jan 2020 15:56:09 +0800 Subject: [PATCH 10/58] remove webpackbar --- package.json | 5 ++-- webpack.config.js | 1 - yarn.lock | 61 +---------------------------------------------- 3 files changed, 3 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index dc645ae8..b7876cb8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "private": true, "scripts": { "dev": "webpack-dev-server", - "build": "webpack --mode=production -p", + "build": "webpack --mode=production -p --progress", "lint": "eslint --ext=js,vue,ts -f=beauty .", "test": "jest" }, @@ -82,8 +82,7 @@ "webpack": "^4.41.2", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1", - "webpack-manifest-plugin": "^2.2.0", - "webpackbar": "^4.0.0" + "webpack-manifest-plugin": "^2.2.0" }, "resolutions": { "caniuse-lite": "*" diff --git a/webpack.config.js b/webpack.config.js index 5aa7c3fb..75bb3c02 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -136,7 +136,6 @@ if (devMode) { config.plugins.push(new webpack.NamedModulesPlugin()) config.plugins.push(new webpack.HotModuleReplacementPlugin()) } else { - config.plugins.push(new WebpackBar()) config.plugins.push(new ManifestPlugin()) } diff --git a/yarn.lock b/yarn.lock index 5af389e0..6cdb2242 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1915,13 +1915,6 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.1.0.tgz#d3ba8047b818293eaaa7978321dd61bff9842cfc" - integrity sha512-Qts4KCLKG+waHc9C4m07weIY8qyeixoS0h6RnbsNVD6Fw+pEZGW3vTyObL3WXpE09Mq4Oi7/lBEyLmOiLtlYWQ== - dependencies: - color-convert "^2.0.1" - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2741,11 +2734,6 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -ci-info@^1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -2852,19 +2840,12 @@ color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3: dependencies: color-name "1.1.3" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2952,11 +2933,6 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -consola@^2.10.0: - version "2.10.1" - resolved "https://registry.npmjs.org/consola/-/consola-2.10.1.tgz#4693edba714677c878d520e4c7e4f69306b4b927" - integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w== - console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -8177,11 +8153,6 @@ pretty-format@^24.0.0, pretty-format@^24.3.0, pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - private@^0.1.6: version "0.1.8" resolved "https://registry.npmjs.org/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -9432,13 +9403,6 @@ static-module@^3.0.2: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -std-env@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/std-env/-/std-env-2.2.1.tgz#2ffa0fdc9e2263e0004c1211966e960948a40f6b" - integrity sha512-IjYQUinA3lg5re/YMlwlfhqNRTzMZMqE+pezevdcTaHceqx8ngEi1alX9nNCk9Sc81fy1fLDeQoaCzeiW1yBOQ== - dependencies: - ci-info "^1.6.0" - stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -10539,20 +10503,6 @@ webpack@^4.41.2: watchpack "^1.6.0" webpack-sources "^1.4.1" -webpackbar@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/webpackbar/-/webpackbar-4.0.0.tgz#ee7a87f16077505b5720551af413c8ecd5b1f780" - integrity sha512-k1qRoSL/3BVuINzngj09nIwreD8wxV4grcuhHTD8VJgUbGcy8lQSPqv+bM00B7F+PffwIsQ8ISd4mIwRbr23eQ== - dependencies: - ansi-escapes "^4.2.1" - chalk "^2.4.2" - consola "^2.10.0" - figures "^3.0.0" - pretty-time "^1.1.0" - std-env "^2.2.1" - text-table "^0.2.0" - wrap-ansi "^6.0.0" - websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -10659,15 +10609,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.0.0.tgz#47c7b7329e0b8000f5756b0693a861e357e4043e" - integrity sha512-8YwLklVkHe4QNpGFrK6Mxm+BaMY7da6C9GlDED3xs3XwThyJHSbVwg9qC4s1N8tBFcnM1S0s8I390RC6SgGe+g== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^5.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From 364608a0c67c3ab4c485018a0c42e28a8b90e458 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 28 Jan 2020 10:38:46 +0800 Subject: [PATCH 11/58] fix webpack config --- webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 75bb3c02..0c9766f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,6 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const TerserJSPlugin = require('terser-webpack-plugin') const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') -const WebpackBar = require('webpackbar') const ManifestPlugin = require('webpack-manifest-plugin') const devMode = !process.argv.includes('-p') From 4953e15369840b3c61ead526e288044a9c686952 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 28 Jan 2020 10:41:10 +0800 Subject: [PATCH 12/58] add new Modal option --- resources/assets/src/components/Modal.tsx | 2 + resources/assets/src/components/ModalBody.tsx | 37 +++---------- .../assets/src/components/ModalContent.tsx | 26 ++++++++++ .../assets/src/components/ModalInput.tsx | 52 +++++++++++++++++++ .../assets/tests/components/Modal.test.tsx | 20 +++++++ 5 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 resources/assets/src/components/ModalContent.tsx create mode 100644 resources/assets/src/components/ModalInput.tsx diff --git a/resources/assets/src/components/Modal.tsx b/resources/assets/src/components/Modal.tsx index 5fb8fa4f..f4206243 100644 --- a/resources/assets/src/components/Modal.tsx +++ b/resources/assets/src/components/Modal.tsx @@ -16,6 +16,7 @@ export type ModalOptions = { placeholder?: string inputType?: string validator?(value: any): string | boolean | undefined + choices?: { text: string; value: string }[] type?: string showHeader?: boolean center?: boolean @@ -114,6 +115,7 @@ const Modal = React.forwardRef( dangerousHTML={props.dangerousHTML} showInput={props.mode === 'prompt'} value={value} + choices={props.choices} onChange={handleInputChange} inputType={props.inputType} placeholder={props.placeholder} diff --git a/resources/assets/src/components/ModalBody.tsx b/resources/assets/src/components/ModalBody.tsx index 8c97a164..ef66893b 100644 --- a/resources/assets/src/components/ModalBody.tsx +++ b/resources/assets/src/components/ModalBody.tsx @@ -1,4 +1,6 @@ import React from 'react' +import ModalContent from './ModalContent' +import ModalInput from './ModalInput' interface Props { text?: string @@ -6,6 +8,7 @@ interface Props { showInput: boolean inputType?: string value?: string + choices?: { text: string; value: string }[] onChange?: React.ChangeEventHandler placeholder?: string invalid?: boolean @@ -13,38 +16,12 @@ interface Props { } const ModalBody: React.FC = props => { - const main = (() => { - if (props.children) { - return props.children - } else if (props.text) { - return props.text.split(/\r?\n/).map((line, i) =>

    {line}

    ) - } else if (props.dangerousHTML) { - return
    - } - })() - return (
    - {main} - {props.showInput && ( - <> -
    - -
    - {props.invalid && ( -
    - - {props.validatorMessage} -
    - )} - - )} + + {props.children} + + {props.showInput && }
    ) } diff --git a/resources/assets/src/components/ModalContent.tsx b/resources/assets/src/components/ModalContent.tsx new file mode 100644 index 00000000..cb0ac8a5 --- /dev/null +++ b/resources/assets/src/components/ModalContent.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +interface Props { + text?: string + dangerousHTML?: string +} + +const ModalContent: React.FC = props => { + if (props.children) { + return <>{props.children} + } else if (props.text) { + return ( + <> + {props.text.split(/\r?\n/).map((line, i) => ( +

    {line}

    + ))} + + ) + } else if (props.dangerousHTML) { + return
    + } + + return <> +} + +export default ModalContent diff --git a/resources/assets/src/components/ModalInput.tsx b/resources/assets/src/components/ModalInput.tsx new file mode 100644 index 00000000..a0dcf4d6 --- /dev/null +++ b/resources/assets/src/components/ModalInput.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +interface Props { + inputType?: string + value?: string + choices?: { text: string; value: string }[] + onChange?: React.ChangeEventHandler + placeholder?: string + invalid?: boolean + validatorMessage?: string +} + +const ModalInput: React.FC = props => ( + <> + {props.inputType === 'radios' && props.choices ? ( + <> + {props.choices.map(choice => ( +
    + + +
    + ))} + + ) : ( +
    + +
    + )} + {props.invalid && ( +
    + + {props.validatorMessage} +
    + )} + +) + +export default ModalInput diff --git a/resources/assets/tests/components/Modal.test.tsx b/resources/assets/tests/components/Modal.test.tsx index 41a9725c..4554f235 100644 --- a/resources/assets/tests/components/Modal.test.tsx +++ b/resources/assets/tests/components/Modal.test.tsx @@ -218,6 +218,26 @@ describe('"prompt" mode', () => { expect(reject).toBeCalled() }) + it('radio inputs', () => { + const choices = [ + { text: 'A', value: 'a' }, + { text: 'B', value: 'b' }, + ] + const resolve = jest.fn() + const { getByText, getByLabelText } = render( + , + ) + fireEvent.click(getByLabelText('B')) + fireEvent.click(getByText(trans('general.confirm'))) + expect(resolve).toBeCalledWith({ value: 'b' }) + }) + it('validate input', () => { const resolve = jest.fn() const reject = jest.fn() From 36a516d58473fd10927cd8cf3d21c77e4315969a Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 28 Jan 2020 16:27:11 +0800 Subject: [PATCH 13/58] add generics at fetch --- resources/assets/src/scripts/net.ts | 48 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/resources/assets/src/scripts/net.ts b/resources/assets/src/scripts/net.ts index c0af72f2..2c70b8a0 100644 --- a/resources/assets/src/scripts/net.ts +++ b/resources/assets/src/scripts/net.ts @@ -28,8 +28,9 @@ export const init: RequestInit = { } function retrieveToken() { - const csrfField = - document.querySelector('meta[name="csrf-token"]') + const csrfField = document.querySelector( + 'meta[name="csrf-token"]', + ) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return csrfField?.content || '' } @@ -40,9 +41,10 @@ export async function walkFetch(request: Request): Promise { try { const response = await fetch(request) const cloned = response.clone() - const body = response.headers.get('Content-Type') === 'application/json' - ? await response.json() - : await response.text() + const body = + response.headers.get('Content-Type') === 'application/json' + ? await response.json() + : await response.text() if (response.ok) { return body } @@ -50,7 +52,9 @@ export async function walkFetch(request: Request): Promise { if (response.status === 422) { // Process validation errors from Laravel. - const { errors }: { + const { + errors, + }: { message: string errors: { [field: string]: string[] } } = body @@ -68,7 +72,7 @@ export async function walkFetch(request: Request): Promise { } if (body.exception && Array.isArray(body.trace)) { - const trace = (body.trace as Array<{ file: string, line: number }>) + const trace = (body.trace as Array<{ file: string; line: number }>) .map((t, i) => `[${i + 1}] ${t.file}#L${t.line}`) .join('
    ') message = `${message}
    ${trace}
    ` @@ -87,7 +91,7 @@ export async function walkFetch(request: Request): Promise { } } -export function get(url: string, params = empty): Promise { +export function get(url: string, params = empty): Promise { emit('beforeFetch', { method: 'GET', url, @@ -96,10 +100,12 @@ export function get(url: string, params = empty): Promise { const qs = queryStringify(params) - return walkFetch(new Request(`${blessing.base_url}${url}${qs && `?${qs}`}`, init)) + return walkFetch( + new Request(`${blessing.base_url}${url}${qs && `?${qs}`}`, init), + ) } -function nonGet(method: string, url: string, data: any): Promise { +function nonGet(method: string, url: string, data: any): Promise { emit('beforeFetch', { method: method.toUpperCase(), url, @@ -119,26 +125,32 @@ function nonGet(method: string, url: string, data: any): Promise { return walkFetch(request) } -export function post(url: string, data = empty): Promise { - return nonGet('POST', url, data) +export function post(url: string, data = empty): Promise { + return nonGet('POST', url, data) } -export function put(url: string, data = empty): Promise { - return nonGet('PUT', url, data) +export function put(url: string, data = empty): Promise { + return nonGet('PUT', url, data) } -export function del(url: string, data = empty): Promise { - return nonGet('DELETE', url, data) +export function del(url: string, data = empty): Promise { + return nonGet('DELETE', url, data) } Vue.use(_Vue => { Object.defineProperty(_Vue.prototype, '$http', { get: () => ({ - get, post, put, del, + get, + post, + put, + del, }), }) }) blessing.fetch = { - get, post, put, del, + get, + post, + put, + del, } From 1d87171808ed2f4d39ffd5970154b576839b48f0 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 31 Jan 2020 15:58:37 +0800 Subject: [PATCH 14/58] rewrite user dashboard with React --- app/Http/Controllers/UserController.php | 4 +- package.json | 3 + .../assets/src/scripts/hooks/useTween.ts | 24 ++ resources/assets/src/scripts/route.ts | 2 +- resources/assets/src/styles/_breakpoints.scss | 27 +++ resources/assets/src/views/user/Dashboard.vue | 212 ------------------ .../src/views/user/Dashboard/InfoBox.tsx | 33 +++ .../src/views/user/Dashboard/SignButton.tsx | 38 ++++ .../assets/src/views/user/Dashboard/index.tsx | 134 +++++++++++ .../src/views/user/Dashboard/score.scss | 23 ++ .../src/views/user/Dashboard/scoreUtils.ts | 34 +++ resources/assets/tests/setup.ts | 4 +- .../assets/tests/views/user/Dashboard.test.ts | 180 --------------- .../tests/views/user/Dashboard.test.tsx | 161 +++++++++++++ .../views/user/widgets/dashboard/usage.twig | 2 +- webpack.config.js | 3 +- yarn.lock | 20 +- 17 files changed, 504 insertions(+), 400 deletions(-) create mode 100644 resources/assets/src/scripts/hooks/useTween.ts create mode 100644 resources/assets/src/styles/_breakpoints.scss delete mode 100644 resources/assets/src/views/user/Dashboard.vue create mode 100644 resources/assets/src/views/user/Dashboard/InfoBox.tsx create mode 100644 resources/assets/src/views/user/Dashboard/SignButton.tsx create mode 100644 resources/assets/src/views/user/Dashboard/index.tsx create mode 100644 resources/assets/src/views/user/Dashboard/score.scss create mode 100644 resources/assets/src/views/user/Dashboard/scoreUtils.ts delete mode 100644 resources/assets/tests/views/user/Dashboard.test.ts create mode 100644 resources/assets/tests/views/user/Dashboard.test.tsx diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 3dcff87f..939e311f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -100,8 +100,8 @@ class UserController extends Controller 'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), 'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')), ], - 'signAfterZero' => option('sign_after_zero'), - 'signGapTime' => option('sign_gap_time'), + 'signAfterZero' => (bool) option('sign_after_zero'), + 'signGapTime' => (int) option('sign_gap_time'), ]); } diff --git a/package.json b/package.json index b7876cb8..815d0554 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,10 @@ "@types/echarts": "^4.4.2", "@types/jest": "^24.0.25", "@types/jquery": "^3.3.29", + "@types/js-yaml": "^3.12.2", "@types/react": "^16.9.17", "@types/react-dom": "^16.9.4", + "@types/tween.js": "^17.2.0", "@types/webpack": "^4.41.2", "@typescript-eslint/eslint-plugin": "^2.8.0", "@typescript-eslint/parser": "^2.8.0", @@ -64,6 +66,7 @@ "file-loader": "^5.0.2", "jest": "^24.9.0", "jest-extended": "^0.11.2", + "js-yaml": "^3.13.1", "mini-css-extract-plugin": "^0.9.0", "optimize-css-assets-webpack-plugin": "^5.0.3", "postcss-loader": "^3.0.0", diff --git a/resources/assets/src/scripts/hooks/useTween.ts b/resources/assets/src/scripts/hooks/useTween.ts new file mode 100644 index 00000000..c9c961f7 --- /dev/null +++ b/resources/assets/src/scripts/hooks/useTween.ts @@ -0,0 +1,24 @@ +import React, { useState, useEffect, useRef } from 'react' +import TWEEN from '@tweenjs/tween.js' + +export default function useTween( + initialValue: T, +): [T, React.Dispatch>] { + const [value, setValue] = useState(initialValue) + const ref = useRef(value) + const [dest, setDest] = useState(initialValue) + + useEffect(() => { + function animate() { + requestAnimationFrame(animate) + TWEEN.update() + setValue(ref.current) + } + + const tween = new TWEEN.Tween(ref) + tween.to({ current: dest }, 1000).start() + animate() + }, [dest]) + + return [value, setDest] +} diff --git a/resources/assets/src/scripts/route.ts b/resources/assets/src/scripts/route.ts index 5d253166..193c107f 100644 --- a/resources/assets/src/scripts/route.ts +++ b/resources/assets/src/scripts/route.ts @@ -10,7 +10,7 @@ export default [ }, { path: 'user', - component: () => import('../views/user/Dashboard.vue'), + react: () => import('../views/user/Dashboard'), el: '#usage-box', }, { diff --git a/resources/assets/src/styles/_breakpoints.scss b/resources/assets/src/styles/_breakpoints.scss new file mode 100644 index 00000000..0be643f5 --- /dev/null +++ b/resources/assets/src/styles/_breakpoints.scss @@ -0,0 +1,27 @@ +@use 'sass:map'; + +$breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 992px, + xl: 1200px, +); + +@mixin less-than($breakpoint) { + @media (max-width: map-get($breakpoints, $breakpoint)) { + @content; + } +} + +@mixin between($down, $up) { + @media (max-width: map-get($breakpoints, $down)) and (min-width: map-get($breakpoints, $up)) { + @content; + } +} + +@mixin greater-than($breakpoint) { + @media (min-width: map-get($breakpoints, $breakpoint)) { + @content; + } +} diff --git a/resources/assets/src/views/user/Dashboard.vue b/resources/assets/src/views/user/Dashboard.vue deleted file mode 100644 index e83f54ce..00000000 --- a/resources/assets/src/views/user/Dashboard.vue +++ /dev/null @@ -1,212 +0,0 @@ - - - - - diff --git a/resources/assets/src/views/user/Dashboard/InfoBox.tsx b/resources/assets/src/views/user/Dashboard/InfoBox.tsx new file mode 100644 index 00000000..6fcb7143 --- /dev/null +++ b/resources/assets/src/views/user/Dashboard/InfoBox.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +interface Props { + name: string + icon: string + color: string + used: number + total: number + unit: string +} + +const InfoBox: React.FC = props => { + const percentage = (props.used / props.total) * 100 + + return ( +
    + + + +
    + {props.name} + + {props.used} / {props.total} {props.unit} + +
    +
    +
    +
    +
    + ) +} + +export default React.memo(InfoBox) diff --git a/resources/assets/src/views/user/Dashboard/SignButton.tsx b/resources/assets/src/views/user/Dashboard/SignButton.tsx new file mode 100644 index 00000000..43e61b4e --- /dev/null +++ b/resources/assets/src/views/user/Dashboard/SignButton.tsx @@ -0,0 +1,38 @@ +import React, { useMemo } from 'react' +import { trans } from '../../../scripts/i18n' +import * as scoreUtils from './scoreUtils' + +interface Props { + isLoading: boolean + lastSign: Date + canSignAfterZero: boolean + signGap: number + onClick: React.MouseEventHandler +} + +const SignButton: React.FC = props => { + const { lastSign, signGap, canSignAfterZero } = props + const remainingTime = useMemo( + () => scoreUtils.remainingTime(lastSign, signGap, canSignAfterZero), + [lastSign, signGap, canSignAfterZero], + ) + const remainingTimeText = useMemo( + () => scoreUtils.remainingTimeText(remainingTime), + [remainingTime], + ) + const canSign = remainingTime <= 0 + + return ( +