Răsfoiți Sursa

Merge branch 'master' into reactify-admin/CustomizePage

# Conflicts:
#	src/client/js/app.jsx
#	src/server/form/index.js
itizawa 6 ani în urmă
părinte
comite
b764f7f336
46 a modificat fișierele cu 501 adăugiri și 429 ștergeri
  1. 1 1
      .github/workflows/build-rc.yml
  2. 2 2
      .github/workflows/build.yml
  3. 7 1
      .github/workflows/ci.yml
  4. 23 11
      .github/workflows/release.yml
  5. 16 2
      CHANGES.md
  6. 4 1
      README.md
  7. 0 6
      bin/github-actions/bump-version.sh
  8. 6 0
      bin/github-actions/update-readme.sh
  9. 0 9
      bin/wercker/init-git.sh
  10. 0 40
      bin/wercker/trigger-growi-docker.sh
  11. 0 28
      bin/wercker/trigger-growi-docs.sh
  12. 1 1
      config/migrate.js
  13. 0 49
      config/webpack.dev.dll.js
  14. 8 6
      config/webpack.dev.js
  15. 4 2
      docker/README.md
  16. 5 4
      package.json
  17. 9 9
      src/client/js/app.jsx
  18. 10 1
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  19. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  20. 21 14
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  21. 22 1
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  22. 12 12
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  23. 10 10
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  24. 16 16
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  25. 13 13
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  26. 1 1
      src/client/js/components/PageEditor/HandsontableModal.jsx
  27. 23 7
      src/client/js/components/User/UserPicture.jsx
  28. 9 1
      src/client/js/components/User/Username.jsx
  29. 43 12
      src/client/js/services/AdminMarkDownContainer.js
  30. 0 8
      src/server/form/admin/markdown.js
  31. 0 8
      src/server/form/admin/markdownPresentation.js
  32. 0 10
      src/server/form/admin/markdownXss.js
  33. 0 15
      src/server/routes/admin.js
  34. 2 3
      src/server/routes/apiv3/import.js
  35. 40 45
      src/server/routes/apiv3/markdown-setting.js
  36. 2 1
      src/server/routes/apiv3/mongo.js
  37. 32 0
      src/server/routes/apiv3/overwrite-params/attachmentFiles.chunks.js
  38. 1 1
      src/server/routes/attachment.js
  39. 4 0
      src/server/service/export.js
  40. 4 23
      src/server/service/growi-bridge.js
  41. 51 26
      src/server/service/import.js
  42. 0 1
      src/server/views/page_presentation.html
  43. 0 1
      src/server/views/widget/headers/scripts-for-dev.html
  44. 37 0
      src/test/config/migrate.test.js
  45. 0 18
      wercker.yml
  46. 61 8
      yarn.lock

+ 1 - 1
.github/workflows/build-rc.yml

@@ -3,7 +3,7 @@ name: Release Docker Images for RC
 on:
   push:
     branches:
-      - rc/*
+      - rc/**
 
 jobs:
 

+ 2 - 2
.github/workflows/build.yml

@@ -34,7 +34,7 @@ jobs:
       run: |
         CACHE_REF=weseek/growi-cache:3${{ env.SUFFIX }}
         docker buildx build \
-          --tag growi \
+          --tag growi${{ env.SUFFIX }} \
           --build-arg flavor=${{ matrix.flavor }} \
           --platform linux/amd64 \
           --load \
@@ -50,7 +50,7 @@ jobs:
     - name: Docker Tags by SemVer
       uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
       with:
-        source: growi
+        source: growi${{ env.SUFFIX }}
         target: weseek/growi
         semver: ${{ env.SEMVER }}
         suffix: ${{ env.SUFFIX }}

+ 7 - 1
.github/workflows/ci.yml

@@ -1,6 +1,12 @@
 name: Node CI
 
-on: [push]
+on:
+  push:
+    branches-ignore:
+      - release/**
+      - tmp/**
+    tags-ignore:
+      - v*.*.*
 
 jobs:
 

+ 23 - 11
.github/workflows/release.yml

@@ -3,10 +3,10 @@ name: GitHub Release
 on:
   push:
     branches:
-      - release
+      - release/**
 
 jobs:
-  build:
+  release:
 
     runs-on: ubuntu-latest
 
@@ -19,19 +19,31 @@ jobs:
       run: |
         git config --local user.name "GitHub Action"
         git config --local user.email "info@weseek.co.jp"
+        git remote set-url origin "https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY"
 
     - name: Bump version
       run: |
-        npm version patch
-        sh ./bin/github-actions/bump-version.sh
+        npm --no-git-tag-version version patch
+        export RELEASE_VERSION=`npm run version --silent`
+        sh ./bin/github-actions/update-readme.sh
+        echo ::set-env name=RELEASE_VERSION::$RELEASE_VERSION
 
-    - name: Commit
+    - name: Checkout, Commit, Tag and Push
       run: |
-        git commit -am "Release $RELEASE_VERSION"
-
-    - name: Push tag
-      uses: ad-m/github-push-action@master
+        TMP_RELEASE_BRANCH=tmp/release-${{ env.RELEASE_VERSION }}
+        git checkout -B $TMP_RELEASE_BRANCH
+        git commit -am "Release v${{ env.RELEASE_VERSION }}"
+        git tag -a v${{ env.RELEASE_VERSION }} -m "v${{ env.RELEASE_VERSION }}"
+        git push --follow-tags origin $TMP_RELEASE_BRANCH
+        echo ::set-env name=TMP_RELEASE_BRANCH::$TMP_RELEASE_BRANCH
+
+    - name: Upload release notes
+      uses: Roang-zero1/github-create-release-action@master
       with:
-        branch: null
-        github_token: ${{ secrets. GITHUB_TOKEN }}
+        created_tag: v${{ env.RELEASE_VERSION }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
+    - name: Remove temporary branch
+      run: |
+        git push --delete origin ${{ env.TMP_RELEASE_BRANCH }}

+ 16 - 2
CHANGES.md

@@ -1,10 +1,12 @@
 # CHANGES
 
-## 3.6.0-RC
+## 3.6.2-RC
+
+## 3.6.1
 
 ### BREAKING CHANGES
 
-* GROWI v3.6.0 no longer support Node.js v8.x
+* GROWI v3.6.x no longer support Node.js v8.x
 * The name of database that is storing migrations meta data has been changed
     * This affects **only when `MONGO_URI` has parameters**
     * v3.5.x or above has a bug ([#1361](https://github.com/weseek/growi/issues/1361))
@@ -14,12 +16,24 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 ### Updates
 
 * Improvement: Drop unnecessary MongoDB collection indexes
+* Improvement: Accessibility of Antarctic theme
 * Fix: Appending tag is failed by wrong index of PageTagRelation
     * Introduced by 3.5.20
+* Fix: Pages without heading slash is invalid but creatable
+* Fix: Connect to Elasticsearch with `httpAuth` param
 * Support: Support Node.js v12
+* Support: Optimize build in dev with hard-source-webpack-plugin
 * Support: Upgrade libs
     * growi-commons
 
+## 3.6.0 (Missing number)
+
+## 3.5.25
+
+* Improvement: Disable ESC key to close Handsontable Modal
+* Fix: Exported data of empty collection is broken
+* Fix: Some components crash after when the page with attachment has exported/imported
+
 ## 3.5.24
 
 * Fix: Plugins are not working on Heroku

+ 4 - 1
README.md

@@ -24,6 +24,10 @@ GROWI
 [![devDependencies Status](https://david-dm.org/weseek/growi/dev-status.svg)](https://david-dm.org/weseek/growi?type=dev)
 [![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
 
+| demonstration |
+| :-: |
+|![sample image](https://user-images.githubusercontent.com/42988650/70600974-6b29cc80-1c34-11ea-94ef-33c39c6a00dc.gif)|
+
 - [Features](#features)
 - [Quick Start for Production](#quick-start-for-production)
     - [Heroku](#heroku)
@@ -33,7 +37,6 @@ GROWI
 - [Documentation](#documentation)
 - [License](#license)
 
-
 Features
 ========
 

+ 0 - 6
bin/github-actions/bump-version.sh

@@ -1,6 +0,0 @@
-#!/bin/sh
-
-cd docker
-
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.5\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.5-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/nocdn\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

+ 6 - 0
bin/github-actions/update-readme.sh

@@ -0,0 +1,6 @@
+#!/bin/sh
+
+cd docker
+
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.6\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.6-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

+ 0 - 9
bin/wercker/init-git.sh

@@ -1,9 +0,0 @@
-#!/bin/sh
-
-git config --global user.name "wercker"
-git config --global user.email "info@weseek.co.jp"
-
-# reconfigure origin
-GITHUB_ORIGIN=https://yuki-takei:$GITHUB_TOKEN@$WERCKER_GIT_DOMAIN/$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY.git
-git remote rm origin
-git remote add origin $GITHUB_ORIGIN

+ 0 - 40
bin/wercker/trigger-growi-docker.sh

@@ -1,40 +0,0 @@
-#!/bin/sh
-
-# Trigger a new run
-# see: http://devcenter.wercker.com/docs/api/endpoints/runs#trigger-a-run
-
-# exec curl
-#
-# require
-#   - $WERCKER_TOKEN
-#   - $GROWI_DOCKER_PIPELINE_ID
-#   - $RELEASE_VERSION
-#   - $RELEASE_GIT_COMMIT
-#
-RESPONSE=`curl -X POST \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer $WERCKER_TOKEN" \
-  https://app.wercker.com/api/v3/runs -d '{ \
-    "pipelineId": "'$GROWI_DOCKER_PIPELINE_ID'", \
-    "branch": "master", \
-    "envVars": [ \
-      { \
-        "key": "RELEASE_VERSION", \
-        "value": "'$RELEASE_VERSION'" \
-      }, \
-      { \
-        "key": "GROWI_REPOS_GIT_COMMIT", \
-        "value": "'$RELEASE_GIT_COMMIT'" \
-      } \
-    ] \
-  }' \
-`
-
-echo $RESPONSE | jq .
-
-# get wercker run id
-RUN_ID=`echo $RESPONSE | jq .id`
-# exit with failure status
-if [ "$RUN_ID" = "null" ]; then
-  exit 1
-fi

+ 0 - 28
bin/wercker/trigger-growi-docs.sh

@@ -1,28 +0,0 @@
-#!/bin/sh
-
-# Trigger a new run
-# see: http://devcenter.wercker.com/docs/api/endpoints/runs#trigger-a-run
-
-# exec curl
-#
-# require
-#   - $WERCKER_TOKEN
-#   - $GROWI_DOCS_PIPELINE_ID
-#
-RESPONSE=`curl -X POST \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer $WERCKER_TOKEN" \
-  https://app.wercker.com/api/v3/runs -d '{ \
-    "pipelineId": "'$GROWI_DOCS_PIPELINE_ID'", \
-    "branch": "master"
-  }' \
-`
-
-echo $RESPONSE | jq .
-
-# get wercker run id
-RUN_ID=`echo $RESPONSE | jq .id`
-# exit with failure status
-if [ "$RUN_ID" = "null" ]; then
-  exit 1
-fi

+ 1 - 1
config/migrate.js

@@ -17,7 +17,7 @@ const mongoUri = getMongoUri();
 const url = new URL(mongoUri);
 
 const mongodb = {
-  url: `${url.protocol}//${url.host}`,
+  url: mongoUri,
   databaseName: url.pathname.substring(1), // omit heading slash
   options: {
     useNewUrlParser: true, // removes a deprecation warning when connecting

+ 0 - 49
config/webpack.dev.dll.js

@@ -1,49 +0,0 @@
-/**
- * @author: Yuki Takei <yuki@weseek.co.jp>
- */
-const webpack = require('webpack');
-const helpers = require('../src/lib/util/helpers');
-
-
-module.exports = {
-  mode: 'development',
-  entry: {
-    dlls: [
-      // Libraries
-      'axios',
-      'browser-bunyan', 'bunyan-format',
-      'codemirror', 'react-codemirror2',
-      'date-fns',
-      'diff2html',
-      'debug',
-      'entities',
-      'growi-commons',
-      'i18next', 'i18next-browser-languagedetector',
-      'jquery-slimscroll',
-      'lodash', 'pako',
-      'markdown-it', 'csv-to-markdown-table',
-      'react', 'react-dom',
-      'react-bootstrap', 'react-bootstrap-typeahead',
-      'react-i18next', 'react-dropzone', 'react-hotkeys', 'react-copy-to-clipboard', 'react-waypoint',
-      'socket.io-client',
-      'toastr',
-      'unstated',
-      'xss',
-    ],
-  },
-  output: {
-    path: helpers.root('public/dll'),
-    filename: 'dll.js',
-    library: 'growi_dlls',
-  },
-  resolve: {
-    extensions: ['.js', '.json'],
-    modules: [helpers.root('src'), helpers.root('node_modules')],
-  },
-  plugins: [
-    new webpack.DllPlugin({
-      path: helpers.root('public/dll/manifest.json'),
-      name: 'growi_dlls',
-    }),
-  ],
-};

+ 8 - 6
config/webpack.dev.js

@@ -2,12 +2,11 @@
  * @author: Yuki Takei <yuki@weseek.co.jp>
  */
 
-const webpack = require('webpack');
-
 /*
  * Webpack Plugins
  */
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 const helpers = require('../src/lib/util/helpers');
 
@@ -59,10 +58,13 @@ module.exports = require('./webpack.common')({
       filename: '[name].bundle.css',
     }),
 
-    new webpack.DllReferencePlugin({
-      context: helpers.root(),
-      manifest: require(helpers.root('public/dll', 'manifest.json')),
-    }),
+    new HardSourceWebpackPlugin(),
+    new HardSourceWebpackPlugin.ExcludeModulePlugin([
+      {
+        // see https://github.com/mzgoddard/hard-source-webpack-plugin/blob/master/README.md#excludemoduleplugin
+        test: /mini-css-extract-plugin[\\/]dist[\\/]loader/,
+      },
+    ]),
 
     new BundleAnalyzerPlugin({
       analyzerMode: ANALYZE ? 'server' : 'disabled',

+ 4 - 2
docker/README.md

@@ -10,8 +10,10 @@ growi-docker
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`3.5.13`, `3.5`, `3`, `latest` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.13/Dockerfile)
-* [`3.5.13-nocdn`, `3.5-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.13/nocdn/Dockerfile)
+* [`3.6.0`, `3.6`, `3`, 'latest', (Dockerfile)](https://github.com/weseek/growi/blob/v3.6.0/docker/Dockerfile)
+* [`3.6.0-nocdn`, `3.6-nocdn`, `3-nocdn`, 'latest-nocdn', (Dockerfile)](https://github.com/weseek/growi/blob/v3.6.0/docker/Dockerfile)
+* [`3.5.25`, `3.5`, `3`, (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.25/Dockerfile)
+* [`3.5.25-nocdn`, `3.5-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.25/nocdn/Dockerfile)
 * [`3.4.7`, `3.4`, `3`, (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.4.7/Dockerfile)
 * [`3.4.7-nocdn`, `3.4-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.4.7/nocdn/Dockerfile)
 

+ 5 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.6.0-RC",
+  "version": "3.6.1-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -23,9 +23,8 @@
     "build:apiv3:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js src/server/**/*.js",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "build:dev:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
-    "build:dev:dll": "webpack --config config/webpack.dev.dll.js",
-    "build:dev:watch": "npm-run-all -s build:dev:dll build:dev:app:watch",
-    "build:dev": "npm-run-all -s build:dev:dll build:dev:app",
+    "build:dev:watch": "npm run build:dev:app:watch",
+    "build:dev": "npm run build:dev:app",
     "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod": "env-cmd -f config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
     "build": "npm run build:dev:watch",
@@ -107,6 +106,7 @@
     "i18next-express-middleware": "^1.4.1",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
+    "is-iso-date": "^0.0.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^7.0.1",
@@ -178,6 +178,7 @@
     "eslint-plugin-react": "^7.14.2",
     "file-loader": "^4.0.0",
     "handsontable": "=6.2.2",
+    "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
     "jest": "^24.8.0",

+ 9 - 9
src/client/js/app.jsx

@@ -53,7 +53,7 @@ import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import UserGroupDetailContainer from './services/UserGroupDetailContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
 import WebsocketContainer from './services/WebsocketContainer';
-import MarkDownSettingContainer from './services/MarkDownSettingContainer';
+import AdminMarkDownContainer from './services/AdminMarkDownContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
 
 const logger = loggerFactory('growi:app');
@@ -118,17 +118,17 @@ if (pageContainer.state.pageId != null) {
   componentMappings = Object.assign({
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
-    'page-attachment':  <PageAttachment />,
-    'page-timeline':  <PageTimeline />,
-    'page-comment-write':  <CommentEditorLazyRenderer />,
+    'page-attachment': <PageAttachment />,
+    'page-timeline': <PageTimeline />,
+    'page-comment-write': <CommentEditorLazyRenderer />,
     'revision-toc': <TableOfContents />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
-    'bookmark-button':  <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />,
-    'bookmark-button-lg':  <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
-    'rename-page-name-input':  <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-    'duplicate-page-name-input':  <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
+    'bookmark-button': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />,
+    'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
+    'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
+    'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
   }, componentMappings);
 }
 if (pageContainer.state.path != null) {
@@ -136,7 +136,7 @@ if (pageContainer.state.path != null) {
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
-    'tag-label':  <TagLabels />,
+    'tag-label': <TagLabels />,
   }, componentMappings);
 }
 

+ 10 - 1
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -15,6 +15,10 @@ import ProgressBar from './Common/ProgressBar';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 
+const IGNORED_COLLECTION_NAMES = [
+  'sessions',
+];
+
 class ExportArchiveDataPage extends React.Component {
 
   constructor(props) {
@@ -46,9 +50,14 @@ class ExportArchiveDataPage extends React.Component {
     ]);
     // TODO: toastSuccess, toastError
 
+    // filter only not ignored collection names
+    const filteredCollections = collections.filter((collectionName) => {
+      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+    });
+
     const { zipFileStats, isExporting, progressList } = status;
     this.setState({
-      collections,
+      collections: filteredCollections,
       zipFileStats,
       isExporting,
       progressList,

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -57,7 +57,7 @@ class UploadForm extends React.Component {
                 type="file"
                 name="file"
                 className="form-control-file"
-                accept=".growi.zip"
+                accept=".zip"
                 ref={this.inputRef}
                 onChange={this.changeFileName}
               />

+ 21 - 14
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -8,7 +8,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 const logger = loggerFactory('growi:importer');
 
@@ -25,7 +25,7 @@ class LineBreakForm extends React.Component {
     const { t } = this.props;
 
     try {
-      await this.props.markDownSettingContainer.updateLineBreakSetting();
+      await this.props.adminMarkDownContainer.updateLineBreakSetting();
       toastSuccess(t('markdown_setting.updated_lineBreak'));
     }
     catch (err) {
@@ -35,8 +35,8 @@ class LineBreakForm extends React.Component {
   }
 
   renderLineBreakOption() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledLinebreaks } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaks } = adminMarkDownContainer.state;
 
     const helpLineBreak = { __html: t('markdown_setting.Enable Line Break desc') };
 
@@ -48,10 +48,10 @@ class LineBreakForm extends React.Component {
               type="checkbox"
               id="isEnabledLinebreaks"
               checked={isEnabledLinebreaks}
-              onChange={() => { markDownSettingContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
             />
             <label htmlFor="isEnabledLinebreaks">
-              { t('markdown_setting.Enable Line Break') }
+              {t('markdown_setting.Enable Line Break')}
             </label>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
@@ -61,8 +61,8 @@ class LineBreakForm extends React.Component {
   }
 
   renderLineBreakInCommentOption() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledLinebreaksInComments } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
 
     const helpLineBreakInComment = { __html: t('markdown_setting.Enable Line Break for comment desc') };
 
@@ -74,10 +74,10 @@ class LineBreakForm extends React.Component {
               type="checkbox"
               id="isEnabledLinebreaksInComments"
               checked={isEnabledLinebreaksInComments}
-              onChange={() => { markDownSettingContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
             />
             <label htmlFor="isEnabledLinebreaksInComments">
-              { t('markdown_setting.Enable Line Break for comment') }
+              {t('markdown_setting.Enable Line Break for comment')}
             </label>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
@@ -87,7 +87,7 @@ class LineBreakForm extends React.Component {
   }
 
   render() {
-    const { t } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
     return (
       <React.Fragment>
@@ -97,7 +97,14 @@ class LineBreakForm extends React.Component {
         </fieldset>
         <div className="form-group my-3">
           <div className="col-xs-offset-4 col-xs-5">
-            <button type="submit" className="btn btn-primary" onClick={this.onClickSubmit}>{ t('Update') }</button>
+            <button
+              type="submit"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={adminMarkDownContainer.state.retrieveError != null}
+            >
+              {t('Update')}
+            </button>
           </div>
         </div>
       </React.Fragment>
@@ -110,13 +117,13 @@ class LineBreakForm extends React.Component {
  * Wrapper component for using unstated
  */
 const LineBreakFormWrapper = (props) => {
-  return createSubscribedElement(LineBreakForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(LineBreakForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 LineBreakForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
 export default withTranslation()(LineBreakFormWrapper);

+ 22 - 1
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -2,15 +2,35 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import loggerFactory from '@alias/logger';
+
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
 import LineBreakForm from './LineBreakForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+
+const logger = loggerFactory('growi:MarkDown');
 
 class MarkdownSetting extends React.Component {
 
+  async componentDidMount() {
+    const { adminMarkDownContainer } = this.props;
+
+    try {
+      await adminMarkDownContainer.retrieveMarkdownData();
+    }
+    catch (err) {
+      toastError(err);
+      adminMarkDownContainer.setState({ retrieveError: err });
+      logger.error(err);
+    }
+
+  }
+
   render() {
     const { t } = this.props;
 
@@ -43,12 +63,13 @@ class MarkdownSetting extends React.Component {
 }
 
 const MarkdownSettingWrapper = (props) => {
-  return createSubscribedElement(MarkdownSetting, props, [AppContainer]);
+  return createSubscribedElement(MarkdownSetting, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 MarkdownSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
 

+ 12 - 12
src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 const logger = loggerFactory('growi:markdown:presentation');
 
@@ -23,7 +23,7 @@ class PresentationForm extends React.Component {
     const { t } = this.props;
 
     try {
-      await this.props.markDownSettingContainer.updatePresentationSetting();
+      await this.props.adminMarkDownContainer.updatePresentationSetting();
       toastSuccess(t('markdown_setting.updated_presentation'));
     }
     catch (err) {
@@ -34,8 +34,8 @@ class PresentationForm extends React.Component {
 
 
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { pageBreakSeparator, pageBreakCustomSeparator } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
 
     return (
       <fieldset className="form-group row my-2">
@@ -49,7 +49,7 @@ class PresentationForm extends React.Component {
             type="radio"
             id="pageBreakOption1"
             checked={pageBreakSeparator === 1}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(1)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
           />
           <label htmlFor="pageBreakOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
@@ -65,7 +65,7 @@ class PresentationForm extends React.Component {
             type="radio"
             id="pageBreakOption2"
             checked={pageBreakSeparator === 2}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(2)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
           />
           <label htmlFor="pageBreakOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
@@ -81,7 +81,7 @@ class PresentationForm extends React.Component {
             type="radio"
             id="pageBreakOption3"
             checked={pageBreakSeparator === 3}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(3)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
           />
           <label htmlFor="pageBreakOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
@@ -89,8 +89,8 @@ class PresentationForm extends React.Component {
               { t('markdown_setting.Custom separator desc') }
               <input
                 className="form-control"
-                value={pageBreakCustomSeparator}
-                onChange={(e) => { markDownSettingContainer.setPageBreakCustomSeparator(e.target.value) }}
+                defaultValue={pageBreakCustomSeparator}
+                onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
               />
             </div>
           </label>
@@ -98,7 +98,7 @@ class PresentationForm extends React.Component {
 
         <div className="form-group my-3">
           <div className="col-xs-offset-4 col-xs-5">
-            <div className="btn btn-primary" onClick={this.onClickSubmit}>{ t('Update') }</div>
+            <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}>{ t('Update') }</div>
           </div>
         </div>
 
@@ -109,13 +109,13 @@ class PresentationForm extends React.Component {
 }
 
 const PresentationFormWrapper = (props) => {
-  return createSubscribedElement(PresentationForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(PresentationForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
 

+ 10 - 10
src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx

@@ -5,13 +5,13 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 class PresentationLineBreakOptions extends React.Component {
 
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { pageBreakOption, customRegularExpression } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakOption, customRegularExpression } = adminMarkDownContainer.state;
 
     return (
       <Fragment>
@@ -20,7 +20,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             id="pageBreakOption1"
             checked={pageBreakOption === 1}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 1 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 1 }) }}
           />
           <label htmlFor="pageBreakOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
@@ -36,7 +36,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             id="pageBreakOption2"
             checked={pageBreakOption === 2}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 2 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 2 }) }}
           />
           <label htmlFor="pageBreakOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
@@ -52,7 +52,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             id="pageBreakOption3"
             checked={pageBreakOption === 3}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 3 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 3 }) }}
           />
           <label htmlFor="pageBreakOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
@@ -60,8 +60,8 @@ class PresentationLineBreakOptions extends React.Component {
               { t('markdown_setting.Custom separator desc') }
               <input
                 className="form-control"
-                value={customRegularExpression}
-                onChange={(e) => { markDownSettingContainer.setState({ customRegularExpression: e.target.value }) }}
+                defaultValue={customRegularExpression}
+                onChange={(e) => { adminMarkDownContainer.setState({ customRegularExpression: e.target.value }) }}
               />
             </div>
           </label>
@@ -73,13 +73,13 @@ class PresentationLineBreakOptions extends React.Component {
 }
 
 const PresentationLineBreakOptionsWrapper = (props) => {
-  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 PresentationLineBreakOptions.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
 

+ 16 - 16
src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -6,52 +6,52 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 class WhiteListInput extends React.Component {
 
   renderRecommendTagBtn() {
-    const { t, markDownSettingContainer } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ tagWhiteList: tags }) }}>
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ tagWhiteList: tags }) }}>
         { t('markdown_setting.import_recommended', 'tags') }
       </p>
     );
   }
 
   renderRecommendAttrBtn() {
-    const { t, markDownSettingContainer } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ attrWhiteList: attrs }) }}>
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ attrWhiteList: attrs }) }}>
         { t('markdown_setting.import_recommended', 'Attrs') }
       </p>
     );
   }
 
   renderTagValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
     if (customizable) {
-      return markDownSettingContainer.state.tagWhiteList;
+      return adminMarkDownContainer.state.tagWhiteList;
     }
 
     return tags;
   }
 
   renderAttrValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
     if (customizable) {
-      return markDownSettingContainer.state.attrWhiteList;
+      return adminMarkDownContainer.state.attrWhiteList;
     }
 
     return attrs;
   }
 
   render() {
-    const { t, customizable, markDownSettingContainer } = this.props;
+    const { t, customizable, adminMarkDownContainer } = this.props;
 
     return (
       <>
@@ -66,8 +66,8 @@ class WhiteListInput extends React.Component {
             rows="6"
             cols="40"
             readOnly={!customizable}
-            value={this.renderTagValue()}
-            onChange={(e) => { markDownSettingContainer.setState({ tagWhiteList: e.target.value }) }}
+            defaultValue={this.renderTagValue()}
+            onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
           />
         </div>
         <div className="m-t-15">
@@ -81,8 +81,8 @@ class WhiteListInput extends React.Component {
             rows="6"
             cols="40"
             readOnly={!customizable}
-            value={this.renderAttrValue()}
-            onChange={(e) => { markDownSettingContainer.setState({ attrWhiteList: e.target.value }) }}
+            defaultValue={this.renderAttrValue()}
+            onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
           />
         </div>
       </>
@@ -92,13 +92,13 @@ class WhiteListInput extends React.Component {
 }
 
 const WhiteListWrapper = (props) => {
-  return createSubscribedElement(WhiteListInput, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(WhiteListInput, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
   customizable: PropTypes.bool.isRequired,
 };

+ 13 - 13
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 import WhiteListInput from './WhiteListInput';
 
@@ -25,7 +25,7 @@ class XssForm extends React.Component {
     const { t } = this.props;
 
     try {
-      await this.props.markDownSettingContainer.updateXssSetting();
+      await this.props.adminMarkDownContainer.updateXssSetting();
       toastSuccess(t('markdown_setting.updated_xss'));
     }
     catch (err) {
@@ -35,8 +35,8 @@ class XssForm extends React.Component {
   }
 
   xssOptions() {
-    const { t, markDownSettingContainer } = this.props;
-    const { xssOption } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { xssOption } = adminMarkDownContainer.state;
 
     return (
       <fieldset className="form-group col-xs-12 my-3">
@@ -46,7 +46,7 @@ class XssForm extends React.Component {
             id="xssOption1"
             name="XssOption"
             checked={xssOption === 1}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 1 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
           />
           <label htmlFor="xssOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
@@ -62,7 +62,7 @@ class XssForm extends React.Component {
             id="xssOption2"
             name="XssOption"
             checked={xssOption === 2}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 2 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
           />
           <label htmlFor="xssOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
@@ -76,7 +76,7 @@ class XssForm extends React.Component {
             id="xssOption3"
             name="XssOption"
             checked={xssOption === 3}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 3 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
           />
           <label htmlFor="xssOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
@@ -88,8 +88,8 @@ class XssForm extends React.Component {
   }
 
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledXss } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledXss } = adminMarkDownContainer.state;
 
     return (
       <React.Fragment>
@@ -103,7 +103,7 @@ class XssForm extends React.Component {
                   className="form-check-input"
                   name="isEnabledXss"
                   checked={isEnabledXss}
-                  onChange={markDownSettingContainer.switchEnableXss}
+                  onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
@@ -114,7 +114,7 @@ class XssForm extends React.Component {
           </div>
           <div className="form-group my-3">
             <div className="col-xs-offset-4 col-xs-5">
-              <div className="btn btn-primary" onClick={this.onClickSubmit}>{ t('Update') }</div>
+              <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}> {t('Update')}</div>
             </div>
           </div>
         </form>
@@ -125,13 +125,13 @@ class XssForm extends React.Component {
 }
 
 const XssFormWrapper = (props) => {
-  return createSubscribedElement(XssForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(XssForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
 export default withTranslation()(XssFormWrapper);

+ 1 - 1
src/client/js/components/PageEditor/HandsontableModal.jsx

@@ -412,7 +412,7 @@ export default class HandsontableModal extends React.PureComponent {
     const dialogClassName = dialogClassNames.join(' ');
 
     return (
-      <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName}>
+      <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName} keyboard={false}>
         <Modal.Header closeButton>
           { this.renderExpandOrContractButton() }
           <Modal.Title>Edit Table</Modal.Title>

+ 23 - 7
src/client/js/components/User/UserPicture.jsx

@@ -2,24 +2,27 @@ import React from 'react';
 import md5 from 'md5';
 import PropTypes from 'prop-types';
 
+const DEFAULT_IMAGE = '/images/icons/user.svg';
+
 // TODO UserComponent?
 export default class UserPicture extends React.Component {
 
   getUserPicture(user) {
+    let pictPath;
+
     // gravatar
     if (user.isGravatarEnabled === true) {
-      return this.generateGravatarSrc(user);
+      pictPath = this.generateGravatarSrc(user);
     }
     // uploaded image
     if (user.image != null) {
-      return user.image;
+      pictPath = user.image;
     }
     if (user.imageAttachment != null) {
       return user.imageAttachment.filePathProxied;
     }
 
-    return '/images/icons/user.svg';
-
+    return pictPath || DEFAULT_IMAGE;
   }
 
   generateGravatarSrc(user) {
@@ -38,9 +41,22 @@ export default class UserPicture extends React.Component {
     return className.join(' ');
   }
 
+  renderForNull() {
+    return (
+      <img
+        src={DEFAULT_IMAGE}
+        alt="someone"
+        className={this.getClassName()}
+      />
+    );
+  }
+
   render() {
     const user = this.props.user;
-    const href = `/user/${user.username}`;
+
+    if (user == null) {
+      return this.renderForNull();
+    }
 
     const imgElem = (
       <img
@@ -53,14 +69,14 @@ export default class UserPicture extends React.Component {
     return (
       (this.props.withoutLink)
         ? <span>{imgElem}</span>
-        : <a href={href}>{imgElem}</a>
+        : <a href={`/user/${user.username}`}>{imgElem}</a>
     );
   }
 
 }
 
 UserPicture.propTypes = {
-  user: PropTypes.object.isRequired,
+  user: PropTypes.object,
   size: PropTypes.string,
   withoutLink: PropTypes.bool,
 };

+ 9 - 1
src/client/js/components/User/Username.jsx

@@ -3,9 +3,17 @@ import PropTypes from 'prop-types';
 
 export default class Username extends React.Component {
 
+  renderForNull() {
+    return <span>anyone</span>;
+  }
+
   render() {
     const { user } = this.props;
 
+    if (user == null) {
+      return this.renderForNull();
+    }
+
     const name = user.name || '(no name)';
     const username = user.username;
     const href = `/user/${user.username}`;
@@ -18,5 +26,5 @@ export default class Username extends React.Component {
 }
 
 Username.propTypes = {
-  user: PropTypes.object.isRequired,
+  user: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), // Possibility of receiving a string of 'null'
 };

+ 43 - 12
src/client/js/services/MarkDownSettingContainer.js → src/client/js/services/AdminMarkDownContainer.js

@@ -1,10 +1,16 @@
 import { Container } from 'unstated';
 
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+const logger = loggerFactory('growi:services:AdminMarkdownContainer');
+
 /**
  * Service container for admin markdown setting page (MarkDownSetting.jsx)
  * @extends {Container} unstated Container
  */
-export default class MarkDownSettingContainer extends Container {
+export default class AdminMarkDownContainer extends Container {
 
   constructor(appContainer) {
     super();
@@ -12,16 +18,15 @@ export default class MarkDownSettingContainer extends Container {
     this.appContainer = appContainer;
 
     this.state = {
-      isEnabledLinebreaks: appContainer.config.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: appContainer.config.isEnabledLinebreaksInComments,
-      pageBreakSeparator: appContainer.config.pageBreakSeparator,
-      pageBreakCustomSeparator: appContainer.config.pageBreakCustomSeparator || '',
-      // pageBreakOption: appContainer.config.pageBreakOption,
-      customRegularExpression: appContainer.config.customRegularExpression || '',
-      isEnabledXss: (appContainer.config.xssOption != null),
-      xssOption: appContainer.config.xssOption,
-      tagWhiteList: appContainer.config.tagWhiteList || '',
-      attrWhiteList: appContainer.config.attrWhiteList || '',
+      retrieveError: null,
+      isEnabledLinebreaks: false,
+      isEnabledLinebreaksInComments: false,
+      pageBreakSeparator: 1,
+      pageBreakCustomSeparator: '',
+      isEnabledXss: false,
+      xssOption: 1,
+      tagWhiteList: '',
+      attrWhiteList: '',
     };
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
@@ -31,7 +36,33 @@ export default class MarkDownSettingContainer extends Container {
    * Workaround for the mangling in production build to break constructor.name
    */
   static getClassName() {
-    return 'MarkDownSettingContainer';
+    return 'AdminMarkDownContainer';
+  }
+
+  /**
+   * retrieve markdown data
+   */
+  async retrieveMarkdownData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/markdown-setting/');
+      const { markdownParams } = response.data;
+
+      this.setState({
+        isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
+        isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
+        pageBreakSeparator: markdownParams.pageBreakSeparator,
+        pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
+        isEnabledXss: markdownParams.isEnabledXss,
+        xssOption: markdownParams.xssOption,
+        tagWhiteList: markdownParams.tagWhiteList || '',
+        attrWhiteList: markdownParams.attrWhiteList || '',
+      });
+
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to fetch data'));
+    }
   }
 
   /**

+ 0 - 8
src/server/form/admin/markdown.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:isEnabledLinebreaks]').trim().toBooleanStrict(),
-  field('markdownSetting[markdown:isEnabledLinebreaksInComments]').trim().toBooleanStrict(),
-);

+ 0 - 8
src/server/form/admin/markdownPresentation.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:presentation:pageBreakSeparator]').trim().toInt(),
-  field('markdownSetting[markdown:presentation:pageBreakCustomSeparator]').trim(),
-);

+ 0 - 10
src/server/form/admin/markdownXss.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:xss:isEnabledPrevention]').trim().toBooleanStrict(),
-  field('markdownSetting[markdown:xss:option]').trim().toInt(),
-  field('markdownSetting[markdown:xss:tagWhiteList]').trim(),
-  field('markdownSetting[markdown:xss:attrWhiteList]').trim(),
-);

+ 0 - 15
src/server/routes/admin.js

@@ -136,21 +136,6 @@ module.exports = function(crowi, app) {
     });
   };
 
-  // app.post('/admin/markdown/presentationSetting' , admin.markdown.presentationSetting);
-  actions.markdown.presentationSetting = async function(req, res) {
-    const markdownSetting = req.form.markdownSetting;
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
-      req.flash('successMessage', ['Successfully updated!']);
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-    }
-
-    return res.redirect('/admin/markdown');
-  };
-
   // app.get('/admin/customize' , admin.customize.index);
   actions.customize = {};
   actions.customize.index = function(req, res) {

+ 2 - 3
src/server/routes/apiv3/import.js

@@ -5,9 +5,6 @@ const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-lin
 const path = require('path');
 const multer = require('multer');
 
-// eslint-disable-next-line no-unused-vars
-const { ObjectId } = require('mongoose').Types;
-
 const express = require('express');
 
 const GrowiArchiveImportOption = require('@commons/models/admin/growi-archive-import-option');
@@ -54,6 +51,8 @@ const generateOverwriteParams = (collectionName, req, options) => {
       return require('./overwrite-params/pages')(req, options);
     case 'revisions':
       return require('./overwrite-params/revisions')(req, options);
+    case 'attachmentFiles.chunks':
+      return require('./overwrite-params/attachmentFiles.chunks')(req, options);
     default:
       return {};
   }

+ 40 - 45
src/server/routes/apiv3/markdown-setting.js

@@ -38,6 +38,8 @@ const validator = {
  *
  *  components:
  *    schemas:
+ *      CustomizeParams:
+ *        type: object
  *      LineBreakParams:
  *        type: object
  *        properties:
@@ -86,6 +88,38 @@ module.exports = (crowi) => {
 
   const { ApiV3FormValidator } = crowi.middlewares;
 
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/:
+   *      get:
+   *        tags: [MarkDownSettind]
+   *        description: Get markdown paramators
+   *        responses:
+   *          200:
+   *            description: params of markdown
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    markdonwParams:
+   *                      $ref: '#/components/schemas/CustomizeParams'
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const markdownParams = {
+      isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+      isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
+      pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
+      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+      tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
+      attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
+    };
+
+    return res.apiv3({ markdownParams });
+  });
+
   /**
    * @swagger
    *
@@ -98,23 +132,14 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
-   *                type: object
-   *                properties:
-   *                  isEnabledLinebreaks:
-   *                    description: enable lineBreak
-   *                    type: boolean
-   *                  isEnabledLinebreaksInComments:
-   *                    description: enable lineBreak in comment
-   *                    type: boolean
+   *                $ref: '#/components/schemas/LineBreakParams'
    *        responses:
    *          200:
    *            description: Succeeded to update lineBreak setting
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/LineBreakParams'
+  *                   $ref: '#/components/schemas/LineBreakParams'
    */
   router.put('/lineBreak', loginRequiredStrictly, adminRequired, csrf, validator.lineBreak, ApiV3FormValidator, async(req, res) => {
 
@@ -151,23 +176,14 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
-   *                type: object
-   *                properties:
-   *                  pageBreakSeparator:
-   *                    description: number of pageBreakSeparator
-   *                    type: number
-   *                  pageBreakCustomSeparator:
-   *                    description: string of pageBreakCustomSeparator
-   *                    type: string
+   *                $ref: '#/components/schemas/PresentationParams'
    *        responses:
    *          200:
    *            description: Succeeded to update presentation setting
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/PresentationParams'
+   *                  $ref: '#/components/schemas/PresentationParams'
    */
   router.put('/presentation', loginRequiredStrictly, adminRequired, csrf, validator.presentationSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
@@ -207,35 +223,14 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
-   *                type: object
-   *                properties:
-   *                  isEnabledPrevention:
-   *                    description: enable xss
-   *                    type: boolean
-   *                  xssOption:
-   *                    description: number of xss option
-   *                    type: number
-   *                  tagWhiteList:
-   *                    description: array of tag whiteList
-   *                    type: array
-   *                    items:
-   *                      type: string
-   *                      description: tag whitelist
-   *                  attrWhiteList:
-   *                    description: array of attr whiteList
-   *                    type: array
-   *                    items:
-   *                      type: string
-   *                      description: attr whitelist
+   *                $ref: '#/components/schemas/XssParams'
    *        responses:
    *          200:
    *            description: Succeeded to update xss setting
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/XssParams'
+   *                  $ref: '#/components/schemas/XssParams'
    */
   router.put('/xss', loginRequiredStrictly, adminRequired, csrf, validator.xssSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.isEnabledXss && req.body.xssOption == null) {

+ 2 - 1
src/server/routes/apiv3/mongo.js

@@ -34,7 +34,8 @@ module.exports = (crowi) => {
    *                      type: string
    */
   router.get('/collections', async(req, res) => {
-    const collections = Object.keys(mongoose.connection.collections);
+    const listCollectionsResult = await mongoose.connection.db.listCollections().toArray();
+    const collections = listCollectionsResult.map(collectionObj => collectionObj.name);
 
     // TODO: use res.apiv3
     return res.json({

+ 32 - 0
src/server/routes/apiv3/overwrite-params/attachmentFiles.chunks.js

@@ -0,0 +1,32 @@
+const { Binary } = require('mongodb');
+const { ObjectId } = require('mongoose').Types;
+
+class AttachmentFilesChunksOverwriteParamsFactory {
+
+  /**
+   * generate overwrite params object
+   * @param {object} req
+   * @param {ImportOptionForPages} option
+   * @return object
+   *  key: property name
+   *  value: any value or a function `(value, { document, schema, propertyName }) => { return newValue }`
+   */
+  static generate(req, option) {
+    const params = {};
+
+    // Date
+    params.files_id = (value, { document, schema, propertyName }) => {
+      return ObjectId(value);
+    };
+
+    // Binary
+    params.data = (value, { document, schema, propertyName }) => {
+      return Binary(value);
+    };
+
+    return params;
+  }
+
+}
+
+module.exports = (req, option) => AttachmentFilesChunksOverwriteParamsFactory.generate(req, option);

+ 1 - 1
src/server/routes/attachment.js

@@ -182,7 +182,7 @@ module.exports = function(crowi, app) {
 
     const attachment = await Attachment.findOne({ filePath });
 
-    return responseForAttachment(res, req.user, attachment);
+    return responseForAttachment(req, res, attachment);
   };
 
   /**

+ 4 - 0
src/server/service/export.js

@@ -139,6 +139,10 @@ class ExportService {
         callback();
       },
       final(callback) {
+        // write beginning brace
+        if (isFirst) {
+          this.push('[');
+        }
         // write ending brace
         this.push(']');
         callback();

+ 4 - 23
src/server/service/growi-bridge.js

@@ -11,26 +11,9 @@ const unzipper = require('unzipper');
 class GrowiBridgeService {
 
   constructor(crowi) {
+    this.crowi = crowi;
     this.encoding = 'utf-8';
     this.metaFileName = 'meta.json';
-
-    // { pages: Page, users: User, ... }
-    this.collectionMap = {};
-    this.initCollectionMap(crowi.models);
-  }
-
-  /**
-   * initialize collection map
-   *
-   * @memberOf GrowiBridgeService
-   * @param {object} models from models/index.js
-   */
-  initCollectionMap(models) {
-    for (const model of Object.values(models)) {
-      if (model.collection != null) {
-        this.collectionMap[model.collection.name] = model;
-      }
-    }
   }
 
   /**
@@ -61,11 +44,9 @@ class GrowiBridgeService {
    * @return {object} instance of mongoose model
    */
   getModelFromCollectionName(collectionName) {
-    const Model = this.collectionMap[collectionName];
-
-    if (Model == null) {
-      throw new Error(`cannot find a model for collection name "${collectionName}"`);
-    }
+    const Model = Object.values(this.crowi.models).find((m) => {
+      return m.collection != null && m.collection.name === collectionName;
+    });
 
     return Model;
   }

+ 51 - 26
src/server/service/import.js

@@ -2,12 +2,17 @@ const logger = require('@alias/logger')('growi:services:ImportService'); // esli
 const fs = require('fs');
 const path = require('path');
 
+const isIsoDate = require('is-iso-date');
+const parseISO = require('date-fns/parseISO');
+
 const { Writable, Transform } = require('stream');
 const JSONStream = require('JSONStream');
 const streamToPromise = require('stream-to-promise');
 const unzipper = require('unzipper');
 
-const { ObjectId } = require('mongoose').Types;
+const mongoose = require('mongoose');
+
+const { ObjectId } = mongoose.Types;
 
 const { createBatchStream } = require('../util/batch-stream');
 const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
@@ -90,16 +95,27 @@ class ImportService {
    *
    * @memberOf ImportService
    * @param {any} value value from imported document
-   * @param {{ document: object, schema: object, key: string }}
+   * @param {{ document: object, schema: object, propertyName: string }}
    * @return {any} new value for the document
    */
   keepOriginal(value, { document, schema, propertyName }) {
-    let _value;
-    if (schema[propertyName].instance === 'ObjectID' && ObjectId.isValid(value)) {
+    let _value = value;
+
+    // _id
+    if (propertyName === '_id' && ObjectId.isValid(value)) {
       _value = ObjectId(value);
     }
-    else {
-      _value = value;
+    // Date
+    else if (isIsoDate(value)) {
+      _value = parseISO(value);
+    }
+
+    // Model
+    if (schema != null) {
+      // ObjectID
+      if (schema[propertyName] != null && schema[propertyName].instance === 'ObjectID' && ObjectId.isValid(value)) {
+        _value = ObjectId(value);
+      }
     }
 
     return _value;
@@ -177,18 +193,20 @@ class ImportService {
     const execUnorderedBulkOpSafely = this.execUnorderedBulkOpSafely.bind(this);
     const emitProgressEvent = this.emitProgressEvent.bind(this);
 
+    const collection = mongoose.connection.collection(collectionName);
+
     const { mode, jsonFileName, overwriteParams } = importSettings;
-    const Model = this.growiBridgeService.getModelFromCollectionName(collectionName);
-    const jsonFile = this.getFile(jsonFileName);
     const collectionProgress = this.currentProgressingStatus.progressMap[collectionName];
 
     try {
+      const jsonFile = this.getFile(jsonFileName);
+
       // validate options
       this.validateImportSettings(collectionName, importSettings);
 
       // flush
       if (mode === 'flushAndInsert') {
-        await Model.remove({});
+        await collection.deleteMany({});
       }
 
       // stream 1
@@ -214,7 +232,7 @@ class ImportService {
       const writeStream = new Writable({
         objectMode: true,
         async write(batch, encoding, callback) {
-          const unorderedBulkOp = Model.collection.initializeUnorderedBulkOp();
+          const unorderedBulkOp = collection.initializeUnorderedBulkOp();
 
           // documents are not persisted until unorderedBulkOp.execute()
           batch.forEach((document) => {
@@ -363,7 +381,8 @@ class ImportService {
     }
     catch (err) {
       result = err.result;
-      errors = err.writeErrors.map((err) => {
+      errors = err.writeErrors || [err];
+      errors.map((err) => {
         const moreDetailErr = err.err;
         return { _id: moreDetailErr.op._id, message: err.errmsg };
       });
@@ -390,27 +409,33 @@ class ImportService {
    */
   convertDocuments(collectionName, document, overwriteParams) {
     const Model = this.growiBridgeService.getModelFromCollectionName(collectionName);
-    const schema = Model.schema.paths;
+    const schema = (Model != null) ? Model.schema.paths : null;
     const convertMap = this.convertMap[collectionName];
 
-    if (convertMap == null) {
-      throw new Error(`attribute map is not defined for ${collectionName}`);
-    }
-
     const _document = {};
 
-    // assign value from documents being imported
-    Object.entries(convertMap).forEach(([propertyName, convertedValue]) => {
-      const value = document[propertyName];
+    // not Mongoose Model
+    if (convertMap == null) {
+      // apply keepOriginal to all of properties
+      Object.entries(document).forEach(([propertyName, value]) => {
+        _document[propertyName] = this.keepOriginal(value, { document, propertyName });
+      });
+    }
+    // Mongoose Model
+    else {
+      // assign value from documents being imported
+      Object.entries(convertMap).forEach(([propertyName, convertedValue]) => {
+        const value = document[propertyName];
 
-      // distinguish between null and undefined
-      if (value === undefined) {
-        return; // next entry
-      }
+        // distinguish between null and undefined
+        if (value === undefined) {
+          return; // next entry
+        }
 
-      const convertFunc = (typeof convertedValue === 'function') ? convertedValue : null;
-      _document[propertyName] = (convertFunc != null) ? convertFunc(value, { document, propertyName, schema }) : convertedValue;
-    });
+        const convertFunc = (typeof convertedValue === 'function') ? convertedValue : null;
+        _document[propertyName] = (convertFunc != null) ? convertFunc(value, { document, propertyName, schema }) : convertedValue;
+      });
+    }
 
     // overwrite documents with custom values
     Object.entries(overwriteParams).forEach(([propertyName, overwriteValue]) => {

+ 0 - 1
src/server/views/page_presentation.html

@@ -22,7 +22,6 @@
     {{ cdnScriptTagsByGroup('basis') }}
 
     {% if env === 'development' %}
-      <script src="/dll/dll.js"></script>
       <script src="{{ webpack_asset('js/dev.js') }}" async></script>
     {% endif %}
 

+ 0 - 1
src/server/views/widget/headers/scripts-for-dev.html

@@ -1,5 +1,4 @@
 {% if env === 'development' %}
-  <script src="/dll/dll.js"></script>
   <script src="{{ webpack_asset('js/dev.js') }}" async></script>
   <!-- Browsersync -->
   <script id="__bs_script__">//<![CDATA[

+ 37 - 0
src/test/config/migrate.test.js

@@ -0,0 +1,37 @@
+describe('config/migrate.js', () => {
+
+  beforeEach(async(done) => {
+    jest.resetModules();
+    done();
+  });
+
+  /* eslint-disable indent */
+  describe.each`
+    MONGO_URI                                         | expectedUrl                                       | expectedDbName
+    ${'mongodb://example.com/growi'}                  | ${'mongodb://example.com/growi'}                  | ${'growi'}
+    ${'mongodb://user:pass@example.com/growi'}        | ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
+    ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
+  `('returns', ({ MONGO_URI, expectedUrl, expectedDbName }) => {
+    test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
+
+      // mock for mongoose-utils
+      jest.doMock('@commons/util/mongoose-utils', () => {
+        return {
+          getMongoUri: () => {
+            return MONGO_URI;
+          },
+        };
+      });
+
+      const { mongoUri, mongodb } = require('@root/config/migrate');
+
+      jest.dontMock('@commons/util/mongoose-utils');
+
+      expect(mongoUri).toBe(MONGO_URI);
+      expect(mongodb.url).toBe(expectedUrl);
+      expect(mongodb.databaseName).toBe(expectedDbName);
+    });
+  });
+  /* eslint-enable indent */
+
+});

+ 0 - 18
wercker.yml

@@ -1,18 +0,0 @@
-box: node:12-slim
-
-test:
-  steps:
-    - script:
-      code: echo "CI processes are migrated to GitHub Actions"
-
-
-build-prod:
-  steps:
-    - script:
-      code: echo "CI processes are migrated to GitHub Actions"
-
-
-build-dev:
-  steps:
-    - script:
-      code: echo "CI processes are migrated to GitHub Actions"

+ 61 - 8
yarn.lock

@@ -3863,6 +3863,11 @@ detect-file@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
 
+detect-indent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
+  integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
+
 detect-libc@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@@ -5836,6 +5841,25 @@ har-validator@~5.1.0:
     ajv "^6.5.5"
     har-schema "^2.0.0"
 
+hard-source-webpack-plugin@^0.13.1:
+  version "0.13.1"
+  resolved "https://registry.yarnpkg.com/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz#a99071e25b232f1438a5bc3c99f10a3869e4428e"
+  integrity sha512-r9zf5Wq7IqJHdVAQsZ4OP+dcUSvoHqDMxJlIzaE2J0TZWn3UjMMrHqwDHR8Jr/pzPfG7XxSe36E7Y8QGNdtuAw==
+  dependencies:
+    chalk "^2.4.1"
+    find-cache-dir "^2.0.0"
+    graceful-fs "^4.1.11"
+    lodash "^4.15.0"
+    mkdirp "^0.5.1"
+    node-object-hash "^1.2.0"
+    parse-json "^4.0.0"
+    pkg-dir "^3.0.0"
+    rimraf "^2.6.2"
+    semver "^5.6.0"
+    tapable "^1.0.0-beta.5"
+    webpack-sources "^1.0.1"
+    write-json-file "^2.3.0"
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -6616,6 +6640,11 @@ is-installed-globally@^0.1.0:
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
 
+is-iso-date@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/is-iso-date/-/is-iso-date-0.0.1.tgz#d1727b0a4f40cf4dd0dbf95a56a58cc991bb76e2"
+  integrity sha1-0XJ7Ck9Az03Q2/laVqWMyZG7duI=
+
 is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -8826,6 +8855,11 @@ node-notifier@^5.2.1, node-notifier@^5.4.0:
     shellwords "^0.1.1"
     which "^1.3.0"
 
+node-object-hash@^1.2.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
+  integrity sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==
+
 node-pre-gyp@^0.10.0:
   version "0.10.0"
   resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
@@ -11390,6 +11424,13 @@ rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1:
   dependencies:
     glob "^7.0.5"
 
+rimraf@^2.6.2:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
 rimraf@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b"
@@ -12692,7 +12733,7 @@ tapable@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
 
-tapable@^1.1.3:
+tapable@^1.0.0-beta.5, tapable@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
@@ -13620,17 +13661,17 @@ webpack-sources@^1.0.0:
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack-sources@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+webpack-sources@^1.0.1, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
+  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
   dependencies:
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
-  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+webpack-sources@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
   dependencies:
     source-list-map "^2.0.0"
     source-map "~0.6.1"
@@ -13815,6 +13856,18 @@ write-file-atomic@^3.0.0:
     signal-exit "^3.0.2"
     typedarray-to-buffer "^3.1.5"
 
+write-json-file@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f"
+  integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=
+  dependencies:
+    detect-indent "^5.0.0"
+    graceful-fs "^4.1.2"
+    make-dir "^1.0.0"
+    pify "^3.0.0"
+    sort-keys "^2.0.0"
+    write-file-atomic "^2.0.0"
+
 write@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"