Ver código fonte

完善系统

RichardSpace369 3 anos atrás
pai
commit
b1302d1ca1

+ 1 - 0
package.json

@@ -47,6 +47,7 @@
     "node-sass": "^5.0.0",
     "rca": "0.0.19",
     "react": "^17.0.1",
+    "react-diff-viewer": "^3.1.1",
     "react-dom": "^17.0.1",
     "react-helmet": "^6.1.0",
     "react-router-dom": "^5.2.0",

+ 1 - 1
src/commons/witherror.tsx

@@ -19,6 +19,6 @@ const withErrorPage: React.FC<Iprops> = ({ error, onRetry, ...props }) => {
       />
     )
   }
-  return props.children
+  return props.children || null
 }
 export default withErrorPage

+ 12 - 10
src/global.d.ts

@@ -1,10 +1,12 @@
-declare module '*.png';
-declare module '*.gif';
-declare module '*.jpg';
-declare module '*.jpeg';
-declare module '*.svg';
-declare module '*.css';
-declare module '*.less';
-declare module '*.scss';
-declare module '*.sass';
-declare module '*.styl';
+declare module '*.png'
+declare module '*.gif'
+declare module '*.jpg'
+declare module '*.jpeg'
+declare module '*.svg'
+declare module '*.css'
+declare module '*.less'
+declare module '*.scss'
+declare module '*.sass'
+declare module '*.styl'
+
+declare module 'react-diff-viewer'

+ 17 - 3
src/page/adminperson/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useState, useEffect } from 'react'
 import { useAntdTable, useRequest } from 'ahooks'
 import { sha256 } from 'js-sha256'
 import { Table, Input, Form, Button, Row, Modal, Result, message } from 'antd'
@@ -109,7 +109,7 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
     params: getListParams,
     pagination,
     run: requestList,
-  } = useAntdTable((params) => {
+  } = useAntdTable(() => {
     return {
       url: '/v1/organization/user_list',
       params: {
@@ -133,6 +133,7 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
       onSuccess() {
         message.success('添加成功')
         requestList({ ...getListParams, ...pagination })
+        form.resetFields()
         changeInsertState({ visible: false })
       },
       onError(error) {
@@ -156,6 +157,7 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
       onSuccess() {
         message.success('更新成功')
         requestList({ ...getListParams, ...pagination })
+        form.resetFields()
         changeEditState({ visible: false })
       },
       onError(error) {
@@ -163,6 +165,11 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
       },
     }
   )
+  useEffect(() => {
+    if (editState.values) {
+      form.setFieldsValue(editState.values)
+    }
+  }, [editState.values])
   /**
    * @description 新增提交
    */
@@ -217,7 +224,12 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
   ]
 
   return (
-    <Witherror error={getListError} onRetry={() => {}}>
+    <Witherror
+      error={getListError}
+      onRetry={() => {
+        requestList({ ...getListParams, ...pagination })
+      }}
+    >
       <Helmet>
         <title>{title}</title>
       </Helmet>
@@ -246,6 +258,7 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
         visible={insertState.visible}
         title='新建管理员'
         onCancel={() => {
+          form.resetFields()
           changeInsertState((preState) => ({ ...preState, visible: false }))
         }}
       >
@@ -265,6 +278,7 @@ const adminperson: React.FC<Iprops> = ({ title, match: { params: routeParams } }
           visible={editState.visible}
           title='编辑管理员'
           onCancel={() => {
+            form.resetFields()
             changeEditState(() => ({ visible: false }))
           }}
         >

+ 98 - 30
src/page/log/index.tsx

@@ -1,46 +1,114 @@
-import React from 'react'
-import { Table, Button } from 'antd'
+import React, { useState } from 'react'
+import { Table, Button, Modal } from 'antd'
 import { Helmet } from 'react-helmet'
+import ReactDiffViewer from 'react-diff-viewer'
 import { useAntdTable } from 'ahooks'
 import { ColumnProps } from 'antd/lib/table'
 import { pageProps } from '../../router/config'
+import WithError from '../../commons/witherror'
+
+interface LogProps {
+  id: number
+  username: string
+  module: string
+  action: string
+  created_at: string
+  origin: string
+  target: string
+}
 
-const colums: ColumnProps<any>[] = [
-  {
-    title: '账户',
-  },
-  {
-    title: '模块',
-  },
-  {
-    title: '行为',
-    dataIndex: 'action',
-  },
-  {
-    title: '名称',
-  },
-  {
-    title: '修改内容',
-    render() {
-      return <Button type='link'>对比</Button>
-    },
-  },
-  {
-    title: '操作时间',
-  },
-]
 type Iprops = pageProps<any>
 const logPage: React.FC<Iprops> = ({ title }) => {
-  const { tableProps } = useAntdTable(() => {}, {
-    manual: true,
-    defaultPageSize: 10,
+  const [modalState, changeModalState] = useState<{ visible: boolean; values?: LogProps }>({
+    visible: false,
   })
+  const { tableProps, error, params, pagination, run } = useAntdTable(
+    (params) => ({
+      url: '/v1/log/list',
+      params,
+    }),
+    {
+      defaultPageSize: 10,
+    }
+  )
+  const colums: ColumnProps<LogProps>[] = [
+    {
+      title: '账户',
+      dataIndex: 'username',
+      key: 'username',
+    },
+    {
+      title: '模块',
+      dataIndex: 'module',
+      key: 'module',
+    },
+    {
+      title: '行为',
+      dataIndex: 'action',
+      key: 'action',
+    },
+
+    {
+      title: '修改内容',
+      render(record) {
+        return (
+          <Button
+            type='link'
+            onClick={() => {
+              changeModalState({
+                visible: true,
+                values: {
+                  ...record,
+                  target: JSON.stringify(JSON.parse(record.target || '""'), null, 2),
+                  origin: JSON.stringify(JSON.parse(record.origin || '""'), null, 2),
+                },
+              })
+            }}
+          >
+            对比
+          </Button>
+        )
+      },
+    },
+    {
+      title: '操作时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+    },
+  ]
   return (
     <>
       <Helmet>
         <title>{title}</title>
       </Helmet>
-      <Table columns={colums} {...tableProps}></Table>
+      <WithError error={error} onRetry={() => run({ ...params, ...pagination })}>
+        <Table
+          rowKey={(record) => record.id}
+          columns={colums}
+          {...tableProps}
+          pagination={{ ...tableProps.pagination, hideOnSinglePage: true }}
+        ></Table>
+      </WithError>
+
+      <Modal
+        maskClosable={false}
+        onCancel={() => {
+          changeModalState({ visible: false })
+        }}
+        width={800}
+        visible={modalState.visible}
+        title='日志对比'
+        footer={null}
+      >
+        <div style={{ height: 600, overflowY: 'scroll' }}>
+          <ReactDiffViewer
+            disableWordDiff={true}
+            oldValue={modalState.values?.origin}
+            newValue={modalState.values?.target}
+            splitView={true}
+          />
+        </div>
+      </Modal>
     </>
   )
 }

+ 2 - 1
src/page/login/index.tsx

@@ -6,7 +6,8 @@ import { useRequest } from 'ahooks'
 import { Helmet } from 'react-helmet'
 import { pageProps } from '../../router/config'
 import './index.scss'
-const loginPage: React.FC<pageProps> = ({ history, title }) => {
+type loginPageProps = pageProps<any>
+const loginPage: React.FC<loginPageProps> = ({ history, title }) => {
   const { loading, run } = useRequest(
     (data) => {
       return {

+ 64 - 54
src/page/mechanism/index.tsx

@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react'
+import React, { useMemo, useState, useEffect } from 'react'
 import {
   Table,
   Space,
@@ -13,7 +13,7 @@ import {
   message,
   Typography,
 } from 'antd'
-import { useRequest, useAntdTable } from 'ahooks'
+import { useRequest } from 'ahooks'
 
 import { withRouter, RouteComponentProps } from 'react-router-dom'
 import { ColumnProps } from 'antd/lib/table'
@@ -45,6 +45,11 @@ const colums: ColumnProps<organizationProps>[] = [
     dataIndex: 'end_time',
     key: 'end_time',
   },
+  {
+    title: '认证key',
+    key: 'key',
+    dataIndex: 'key',
+  },
   {
     title: '过期状态',
     dataIndex: 'is_expire',
@@ -58,7 +63,7 @@ const colums: ColumnProps<organizationProps>[] = [
     dataIndex: 'is_disable',
     key: 'is_disable',
     render(dataKey) {
-      return dataKey ? <Text type='success'>已启用</Text> : <Text type='warning'>已停用</Text>
+      return dataKey ? <Text type='warning'>已停用</Text> : <Text type='success'>已启用</Text>
     },
   },
 ]
@@ -69,6 +74,7 @@ const colums: ColumnProps<organizationProps>[] = [
  * @returns
  */
 const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
+  const [form] = Form.useForm()
   const [{ filter, page }, changeSearchState] = useState({
     filter: '',
     page: 1,
@@ -78,10 +84,15 @@ const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
     visible: boolean
     values?: organizationProps
   }>({ visible: false, values: undefined })
-  const [form] = Form.useForm()
+  useEffect(() => {
+    if (editModalState.values) {
+      form.setFieldsValue(editModalState.values)
+    }
+  }, [editModalState.values])
   const {
     error: requestListError,
     run,
+    params,
     data: listData = { page: 1, total: 0, list: [] },
   } = useRequest(
     (params) => {
@@ -133,7 +144,7 @@ const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
         message.error(error.message)
       },
       onSuccess() {
-        message.success('新机构成功!')
+        message.success('新机构成功!')
         changeEditModalState({ visible: false })
         run({ filter, page })
       },
@@ -184,54 +195,51 @@ const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
       },
     })
   }, [])
-  const InsertForms = ({ initValues }: { initValues?: any }) => {
-    return (
-      <Form labelCol={{ span: 5 }} initialValues={initValues} form={form}>
-        <Form.Item
-          name='organization_name'
-          label='账号名称'
-          required
-          rules={[
-            {
-              required: true,
-              message: '请输入账号名称',
-            },
-          ]}
-        >
-          <Input placeholder='请输入账号名称'></Input>
-        </Form.Item>
-        <Form.Item
-          name='month'
-          label='有效期'
-          required
-          rules={[
-            {
-              required: true,
-              message: '请设置有效期',
-            },
-          ]}
-        >
-          <InputNumber
-            min={0}
-            formatter={(value) => `${value} 月`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
-            placeholder='请设置有效期'
-            style={{ width: '100%' }}
-          ></InputNumber>
-        </Form.Item>
-        <Form.Item
-          normalize={(value) => Boolean(value)}
-          name='is_disable'
-          label='启用'
-          valuePropName='checked'
-        >
-          <Switch></Switch>
-        </Form.Item>
-      </Form>
-    )
-  }
-
+  const InsertForms = (
+    <Form labelCol={{ span: 5 }} form={form}>
+      <Form.Item
+        name='organization_name'
+        label='账号名称'
+        required
+        rules={[
+          {
+            required: true,
+            message: '请输入账号名称',
+          },
+        ]}
+      >
+        <Input placeholder='请输入账号名称'></Input>
+      </Form.Item>
+      <Form.Item
+        name='month'
+        label='有效期(增)'
+        required
+        rules={[
+          {
+            required: true,
+            message: '请设置有效期',
+          },
+        ]}
+      >
+        <InputNumber
+          min={0}
+          formatter={(value) => `${value} 月`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
+          placeholder='请设置有效期'
+          style={{ width: '100%' }}
+        ></InputNumber>
+      </Form.Item>
+      <Form.Item
+        normalize={(value) => Boolean(value)}
+        name='is_disable'
+        label='停用'
+        valuePropName='checked'
+      >
+        <Switch></Switch>
+      </Form.Item>
+    </Form>
+  )
   return (
-    <WithErrorPage error={requestListError} onRetry={() => {}}>
+    <WithErrorPage error={requestListError} onRetry={() => run(params)}>
       <Row style={{ margin: '20px 0' }} justify='space-between'>
         <Col span={4}>
           <Search
@@ -276,10 +284,11 @@ const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
         visible={insertModalState.visible}
         onOk={handleSubmitAdd}
         onCancel={() => {
+          form.resetFields()
           changeInsertModalState((preState) => ({ ...preState, visible: false }))
         }}
       >
-        <InsertForms></InsertForms>
+        {InsertForms}
       </Modal>
       {/* 编辑窗口 */}
       {editModalState.values && Object.keys(editModalState.values).length ? (
@@ -292,11 +301,12 @@ const mechanism: React.FC<RouteComponentProps> = ({ history }) => {
           destroyOnClose
           visible={editModalState.visible}
           onCancel={() => {
+            form.resetFields()
             changeEditModalState(() => ({ visible: false }))
           }}
           onOk={handleSubmitUpdate}
         >
-          <InsertForms initValues={editModalState.values}></InsertForms>
+          {InsertForms}
         </Modal>
       ) : null}
     </WithErrorPage>

+ 18 - 11
src/page/personcenter/index.tsx

@@ -2,6 +2,7 @@ import React from 'react'
 import { Form, Input, Button, message } from 'antd'
 import { Helmet } from 'react-helmet'
 import { useRequest } from 'ahooks'
+import { sha256 } from 'js-sha256'
 import { pageProps } from '../../router/config'
 
 type Iprops = pageProps<any>
@@ -11,18 +12,24 @@ type Iprops = pageProps<any>
  * @returns
  */
 const personCenter: React.FC<Iprops> = ({ title }) => {
+  const [form] = Form.useForm()
   const { run, loading } = useRequest(
     (data) => {
       return {
-        method: 'POST',
-        data,
-        url: '',
+        method: 'PUT',
+        data: {
+          ...data,
+          old: sha256(data.old),
+          new: sha256(data.new),
+        },
+        url: '/v1/user/password',
       }
     },
     {
       manual: true,
       onSuccess() {
-        message.success('密码修改成功')
+        message.success('密码修改成功!')
+        form.resetFields()
       },
       onError(error) {
         message.error(error.message)
@@ -37,30 +44,30 @@ const personCenter: React.FC<Iprops> = ({ title }) => {
       <Form labelCol={{ span: 6 }} wrapperCol={{ span: 6 }} onFinish={run}>
         <Form.Item
           label='旧密码'
-          name='old_password'
+          name='old'
           required
           rules={[{ required: true, message: '请输入旧密码' }]}
         >
-          <Input type='password' placeholder='请输入旧密码'></Input>
+          <Input.Password placeholder='请输入旧密码'></Input.Password>
         </Form.Item>
         <Form.Item
           label='新密码'
-          name='password'
+          name='new'
           required
           rules={[{ required: true, message: '请输入新密码' }]}
         >
-          <Input type='password' placeholder='请输入新密码'></Input>
+          <Input.Password placeholder='请输入新密码'></Input.Password>
         </Form.Item>
         <Form.Item
           label='重复密码'
           name='confirm_password'
           required
-          dependencies={['password']}
+          dependencies={['new']}
           rules={[
             { required: true, message: '重复输入新密码' },
             ({ getFieldValue }) => ({
               validator(_, value) {
-                if (!value || getFieldValue('password') === value) {
+                if (!value || getFieldValue('new') === value) {
                   return Promise.resolve()
                 }
                 return Promise.reject(new Error('密码不一致!'))
@@ -68,7 +75,7 @@ const personCenter: React.FC<Iprops> = ({ title }) => {
             }),
           ]}
         >
-          <Input type='password' placeholder='重复输入新密码'></Input>
+          <Input.Password placeholder='重复输入新密码'></Input.Password>
         </Form.Item>
         <Form.Item wrapperCol={{ offset: 6, span: 6 }}>
           <Button block type='primary' htmlType='submit' loading={loading}>

+ 135 - 4
yarn.lock

@@ -930,7 +930,7 @@
     "@babel/helper-validator-option" "^7.12.17"
     "@babel/plugin-transform-typescript" "^7.13.0"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
   version "7.13.17"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec"
   integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==
@@ -983,6 +983,62 @@
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f"
   integrity sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==
 
+"@emotion/cache@^10.0.27":
+  version "10.0.29"
+  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
+  integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+  dependencies:
+    "@emotion/sheet" "0.9.4"
+    "@emotion/stylis" "0.8.5"
+    "@emotion/utils" "0.11.3"
+    "@emotion/weak-memoize" "0.2.5"
+
+"@emotion/hash@0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+  integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/memoize@0.7.4":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
+  version "0.11.16"
+  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
+  integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+  dependencies:
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/unitless" "0.7.5"
+    "@emotion/utils" "0.11.3"
+    csstype "^2.5.7"
+
+"@emotion/sheet@0.9.4":
+  version "0.9.4"
+  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
+  integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+
+"@emotion/stylis@0.8.5":
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
+  integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+
+"@emotion/unitless@0.7.5":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
+  integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/utils@0.11.3":
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
+  integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+
+"@emotion/weak-memoize@0.2.5":
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+  integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
 "@eslint/eslintrc@^0.4.0":
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547"
@@ -1831,6 +1887,22 @@ babel-plugin-dynamic-import-node@^2.3.3:
   dependencies:
     object.assign "^4.1.0"
 
+babel-plugin-emotion@^10.0.27:
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d"
+  integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@emotion/hash" "0.8.0"
+    "@emotion/memoize" "0.7.4"
+    "@emotion/serialize" "^0.11.16"
+    babel-plugin-macros "^2.0.0"
+    babel-plugin-syntax-jsx "^6.18.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^1.0.5"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
+
 babel-plugin-import@^1.13.3:
   version "1.13.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-import/-/babel-plugin-import-1.13.3.tgz#9dbbba7d1ac72bd412917a830d445e00941d26d7"
@@ -1839,6 +1911,15 @@ babel-plugin-import@^1.13.3:
     "@babel/helper-module-imports" "^7.0.0"
     "@babel/runtime" "^7.0.0"
 
+babel-plugin-macros@^2.0.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
+  integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
+  dependencies:
+    "@babel/runtime" "^7.7.2"
+    cosmiconfig "^6.0.0"
+    resolve "^1.12.0"
+
 babel-plugin-polyfill-corejs2@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz#686775bf9a5aa757e10520903675e3889caeedc4"
@@ -1863,6 +1944,11 @@ babel-plugin-polyfill-regenerator@^0.2.0:
   dependencies:
     "@babel/helper-define-polyfill-provider" "^0.2.0"
 
+babel-plugin-syntax-jsx@^6.18.0:
+  version "6.18.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+  integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -2342,7 +2428,7 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.7.0:
+convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
@@ -2411,6 +2497,16 @@ cosmiconfig@^7.0.0:
     path-type "^4.0.0"
     yaml "^1.10.0"
 
+create-emotion@^10.0.14, create-emotion@^10.0.27:
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503"
+  integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==
+  dependencies:
+    "@emotion/cache" "^10.0.27"
+    "@emotion/serialize" "^0.11.15"
+    "@emotion/sheet" "0.9.4"
+    "@emotion/utils" "0.11.3"
+
 create-error-class@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
@@ -2507,6 +2603,11 @@ cssesc@^3.0.0:
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
+csstype@^2.5.7:
+  version "2.6.17"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e"
+  integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==
+
 csstype@^3.0.2:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
@@ -2832,6 +2933,14 @@ emojis-list@^3.0.0:
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
 
+emotion@^10.0.14:
+  version "10.0.27"
+  resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e"
+  integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==
+  dependencies:
+    babel-plugin-emotion "^10.0.27"
+    create-emotion "^10.0.27"
+
 encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -3331,6 +3440,11 @@ find-cache-dir@^3.3.1:
     make-dir "^3.0.2"
     pkg-dir "^4.1.0"
 
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4783,6 +4897,11 @@ memfs@^3.1.2:
   dependencies:
     fs-monkey "1.0.3"
 
+memoize-one@^5.0.4:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
+  integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
+
 memory-fs@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -6421,6 +6540,18 @@ rca@0.0.19:
     yeoman-environment "^2.10.3"
     yeoman-generator "^4.12.0"
 
+react-diff-viewer@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz#21ac9c891193d05a3734bfd6bd54b107ee6d46cc"
+  integrity sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==
+  dependencies:
+    classnames "^2.2.6"
+    create-emotion "^10.0.14"
+    diff "^4.0.1"
+    emotion "^10.0.14"
+    memoize-one "^5.0.4"
+    prop-types "^15.6.2"
+
 react-dom@^17.0.1:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -6750,7 +6881,7 @@ resolve-url@^0.2.1:
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2:
+resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -7159,7 +7290,7 @@ source-map@^0.4.2:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.5.0, source-map@^0.5.6:
+source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=