Browse Source

Revert "master merge"

This reverts commit 446a059587121ce90949683b45944ece607e412d, reversing
changes made to d53205598a941fc0311d486b73c212ec902f8306.
KazuyaNagase 6 years ago
parent
commit
162d968472
100 changed files with 1239 additions and 7778 deletions
  1. 1 1
      .eslintignore
  2. 0 54
      .github/workflows/build-rc.yml
  3. 0 82
      .github/workflows/build.yml
  4. 0 239
      .github/workflows/ci.yml
  5. 0 45
      .github/workflows/release.yml
  6. 0 9
      .markdownlint.yml
  7. 3 3
      .stylelintrc.json
  8. 0 1
      .vscode/extensions.json
  9. 2 7
      .vscode/launch.json
  10. 6 15
      .vscode/settings.json
  11. 11 134
      CHANGES.md
  12. 148 48
      README.md
  13. 5 5
      app.json
  14. 0 6
      bin/github-actions/update-readme.sh
  15. 0 3
      bin/heroku/install-packages.sh
  16. 7 0
      bin/heroku/install-plugins.sh
  17. 9 0
      bin/wercker/init-git.sh
  18. 40 0
      bin/wercker/trigger-growi-docker.sh
  19. 28 0
      bin/wercker/trigger-growi-docs.sh
  20. 8 10
      config/jest.config.js
  21. 16 16
      config/migrate.js
  22. 2 4
      config/swagger-definition.js
  23. 0 9
      config/webpack.common.js
  24. 49 0
      config/webpack.dev.dll.js
  25. 7 0
      config/webpack.dev.js
  26. 0 118
      docker/Dockerfile
  27. 0 13
      docker/Dockerfile.dockerignore
  28. 0 93
      docker/README.md
  29. 0 7
      docker/bin/remove-except-artifacts.sh
  30. 0 14
      docker/docker-entrypoint.sh
  31. 0 5
      docker/nocdn/env.prod.js
  32. 26 34
      package.json
  33. 90 138
      resource/locales/en-US/translation.json
  34. 82 131
      resource/locales/ja/translation.json
  35. 66 96
      src/client/js/app.jsx
  36. 0 68
      src/client/js/components/Admin/AdminHome/AdminHome.jsx
  37. 0 53
      src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx
  38. 0 53
      src/client/js/components/Admin/AdminHome/SystemInfomationTable.jsx
  39. 82 0
      src/client/js/components/Admin/AdminRebuildSearch.jsx
  40. 0 139
      src/client/js/components/Admin/App/AppSetting.jsx
  41. 0 94
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  42. 0 156
      src/client/js/components/Admin/App/AwsSetting.jsx
  43. 0 117
      src/client/js/components/Admin/App/MailSetting.jsx
  44. 0 80
      src/client/js/components/Admin/App/PluginSetting.jsx
  45. 0 108
      src/client/js/components/Admin/App/SiteUrlSetting.jsx
  46. 0 28
      src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx
  47. 0 45
      src/client/js/components/Admin/Common/ProgressBar.jsx
  48. 5 4
      src/client/js/components/Admin/CustomCssEditor.jsx
  49. 5 4
      src/client/js/components/Admin/CustomHeaderEditor.jsx
  50. 5 4
      src/client/js/components/Admin/CustomScriptEditor.jsx
  51. 0 85
      src/client/js/components/Admin/Customize/Customize.jsx
  52. 0 38
      src/client/js/components/Admin/Customize/CustomizeBehaviorOption.jsx
  53. 0 99
      src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx
  54. 0 91
      src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx
  55. 0 38
      src/client/js/components/Admin/Customize/CustomizeFunctionOption.jsx
  56. 0 158
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  57. 0 100
      src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx
  58. 0 143
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  59. 0 42
      src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx
  60. 0 81
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  61. 0 79
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  62. 0 122
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  63. 0 91
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  64. 0 79
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  65. 0 45
      src/client/js/components/Admin/Customize/ThemeColorBox.jsx
  66. 138 0
      src/client/js/components/Admin/Export/ExportPage.jsx
  67. 5 5
      src/client/js/components/Admin/Export/ExportTableMenu.jsx
  68. 162 0
      src/client/js/components/Admin/Export/ExportZipFormModal.jsx
  69. 9 10
      src/client/js/components/Admin/Export/ZipFileTable.jsx
  70. 0 245
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  71. 0 261
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  72. 0 35
      src/client/js/components/Admin/FullTextSearchManagement.jsx
  73. 0 133
      src/client/js/components/Admin/FullTextSearchManagement/RebuildIndex.jsx
  74. 181 0
      src/client/js/components/Admin/Import/GrowiZipImportForm.jsx
  75. 30 30
      src/client/js/components/Admin/Import/GrowiZipImportSection.jsx
  76. 11 11
      src/client/js/components/Admin/Import/GrowiZipUploadForm.jsx
  77. 0 52
      src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  78. 0 228
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  79. 0 253
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  80. 0 507
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  81. 0 347
      src/client/js/components/Admin/ImportDataPage.jsx
  82. 0 80
      src/client/js/components/Admin/ManageExternalAccount.jsx
  83. 0 129
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  84. 0 115
      src/client/js/components/Admin/MarkdownSetting/LineBreakSetting.jsx
  85. 0 76
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  86. 0 122
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  87. 0 86
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  88. 0 89
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  89. 0 163
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  90. 0 120
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  91. 0 206
      src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx
  92. 0 186
      src/client/js/components/Admin/UserGroup/UserGroupPage.jsx
  93. 0 134
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  94. 0 37
      src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  95. 0 37
      src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx
  96. 0 51
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx
  97. 0 106
      src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx
  98. 0 87
      src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  99. 0 164
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  100. 0 89
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

+ 1 - 1
.eslintignore

@@ -4,5 +4,5 @@
 /public/**
 /public/**
 /src/client/js/legacy/thirdparty-js/**
 /src/client/js/legacy/thirdparty-js/**
 /src/client/js/util/reveal/plugins/markdown.js
 /src/client/js/util/reveal/plugins/markdown.js
-/src/linter-checker/**
+/test/**
 /tmp/**
 /tmp/**

+ 0 - 54
.github/workflows/build-rc.yml

@@ -1,54 +0,0 @@
-name: Release Docker Images for RC
-
-on:
-  push:
-    branches:
-      - rc/**
-
-jobs:
-
-  build-rc:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v1
-
-    - name: Cache/Restore yarn cache
-      uses: actions/cache@v1
-      with:
-        path: /usr/local/share/.cache/yarn
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-
-    - name: Set up Docker Buildx
-      uses: crazy-max/ghaction-docker-buildx@v1.0.4
-
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
-
-    - name: Build Docker Image
-      run: |
-        CACHE_REF=weseek/growi-cache:3
-        docker buildx build \
-          --tag growi \
-          --platform linux/amd64 \
-          --load \
-          --cache-from=type=registry,ref=$CACHE_REF \
-          --cache-to=type=registry,ref=$CACHE_REF,mode=max \
-          --file ./docker/Dockerfile .
-
-    - name: Get SemVer
-      run: |
-        semver=`npm run version --silent`
-        echo ::set-env name=SEMVER::$semver
-
-    - name: Docker Tags by SemVer
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
-      with:
-        source: growi
-        target: weseek/growi
-        semver: ${{ env.SEMVER }}
-        publish: true

+ 0 - 82
.github/workflows/build.yml

@@ -1,82 +0,0 @@
-name: Release Docker Images
-
-on:
-  push:
-    tags:
-      - v3.*.*
-
-jobs:
-
-  build:
-
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        flavor: [default, nocdn]
-
-    steps:
-    - uses: actions/checkout@v1
-
-    - name: Determine suffix
-      run: |
-        [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo ::set-env name=SUFFIX::$suffix
-
-    - name: Cache/Restore yarn cache
-      uses: actions/cache@v1
-      with:
-        path: /usr/local/share/.cache/yarn
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-
-    - name: Set up Docker Buildx
-      uses: crazy-max/ghaction-docker-buildx@v1.0.4
-
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
-
-    - name: Build Docker Image
-      run: |
-        CACHE_REF=weseek/growi-cache:3${{ env.SUFFIX }}
-        docker buildx build \
-          --tag growi${{ env.SUFFIX }} \
-          --build-arg flavor=${{ matrix.flavor }} \
-          --platform linux/amd64 \
-          --load \
-          --cache-from=type=registry,ref=$CACHE_REF \
-          --cache-to=type=registry,ref=$CACHE_REF,mode=max \
-          --file ./docker/Dockerfile .
-
-    - name: Get SemVer
-      run: |
-        semver=`npm run version --silent`
-        echo ::set-env name=SEMVER::$semver
-
-    - name: Docker Tags by SemVer
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
-      with:
-        source: growi${{ env.SUFFIX }}
-        target: weseek/growi
-        semver: ${{ env.SEMVER }}
-        suffix: ${{ env.SUFFIX }}
-        additional-tags: 'latest'
-        publish: true
-
-  publish-desc:
-
-    runs-on: ubuntu-latest
-    needs: build
-
-    steps:
-    - uses: actions/checkout@v1
-
-    - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v2.1.0
-      env:
-        DOCKERHUB_USERNAME: wsmoogle
-        DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
-        DOCKERHUB_REPOSITORY: weseek/growi
-        README_FILEPATH: ./docker/README.md

+ 0 - 239
.github/workflows/ci.yml

@@ -1,239 +0,0 @@
-name: Node CI
-
-on:
-  push:
-    branches-ignore:
-      - release/**
-      - tmp/**
-
-jobs:
-
-  lint:
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        node-version: [12.x]
-
-    steps:
-    - uses: actions/checkout@v1
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v1
-      with:
-        path: node_modules
-        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-    - name: Get yarn cache dir
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      id: cache-yarn
-      run: echo "::set-output name=dir::$(yarn cache dir)"
-    - name: Cache/Restore yarn cache
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      uses: actions/cache@v1
-      with:
-        path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-    - name: Install dependencies
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-    - name: yarn lint
-      run: |
-        yarn lint
-
-    - name: Slack Notification
-      uses: homoluctus/slatify@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
-        channel: '#ci'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-
-  test:
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        node-version: [12.x]
-
-    steps:
-    - uses: actions/checkout@v1
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v1
-      with:
-        path: node_modules
-        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-    - name: Get yarn cache dir
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      id: cache-yarn
-      run: echo "::set-output name=dir::$(yarn cache dir)"
-    - name: Cache/Restore yarn cache
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      uses: actions/cache@v1
-      with:
-        path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-    - name: Install dependencies
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-    - name: Launch MongoDB
-      uses: wbari/start-mongoDB@v0.2
-      with:
-        mongoDBVersion: 3.6
-    - name: yarn test
-      run: |
-        yarn test
-      env:
-        MONGO_URI: mongodb://localhost:27017/growi_test
-
-    - name: Slack Notification
-      uses: homoluctus/slatify@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
-        channel: '#ci'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-
-  build-dev:
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        node-version: [12.x]
-
-    steps:
-    - uses: actions/checkout@v1
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v1
-      with:
-        path: node_modules
-        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-    - name: Get yarn cache dir
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      id: cache-yarn
-      run: echo "::set-output name=dir::$(yarn cache dir)"
-    - name: Cache/Restore yarn cache
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      uses: actions/cache@v1
-      with:
-        path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-    - name: Install dependencies
-      if: steps.cache-dependencies.outputs.cache-hit != 'true'
-      run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-    - name: yarn build:dev
-      run: |
-        yarn build:dev
-
-    - name: Slack Notification
-      uses: homoluctus/slatify@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*build-dev (${{ matrix.node-version }})*'
-        channel: '#ci'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-
-  build-prod:
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        node-version: [10.x, 12.x]
-
-    steps:
-    - uses: actions/checkout@v1
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: Get yarn cache dir
-      id: cache-yarn
-      run: echo "::set-output name=dir::$(yarn cache dir)"
-    - name: Cache/Restore yarn cache
-      uses: actions/cache@v1
-      with:
-        path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-    - name: Install dependencies
-      run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-        yarn add -D react-images@1.0.0 react-motion
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-    - name: yarn build:prod
-      run: |
-        yarn build:prod
-    - name: yarn install --production
-      run: |
-        yarn install --production
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --production --depth=0
-    - name: Launch MongoDB
-      uses: wbari/start-mongoDB@v0.2
-      with:
-        mongoDBVersion: 3.6
-    - name: yarn server:prod:ci
-      run: |
-        yarn server:prod:ci
-      env:
-        MONGO_URI: mongodb://localhost:27017/growi
-
-    - name: Slack Notification
-      uses: homoluctus/slatify@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*build-prod (${{ matrix.node-version }})*'
-        channel: '#ci'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 0 - 45
.github/workflows/release.yml

@@ -1,45 +0,0 @@
-name: GitHub Release
-
-on:
-  push:
-    branches:
-      - release/**
-
-jobs:
-  release:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
-
-    - name: Init Git
-      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 --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: Checkout, Commit, Tag and Push
-      run: |
-        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
-        git push --delete origin $TMP_RELEASE_BRANCH
-
-    - name: Upload release notes
-      uses: Roang-zero1/github-create-release-action@master
-      with:
-        created_tag: v${{ env.RELEASE_VERSION }}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 0 - 9
.markdownlint.yml

@@ -1,9 +0,0 @@
-ul-indent:
-  indent: 4
-ul-style: false
-heading-style: false
-line-length: false
-no-multiple-blanks: false
-no-duplicate-heading: false
-no-inline-html: false
-no-trailing-punctuation: false

+ 3 - 3
.stylelintrc.json

@@ -1,10 +1,10 @@
 {
 {
   "extends": [
   "extends": [
-    "stylelint-config-recess-order"
+    "stylelint-config-recess-order",
+    "./node_modules/prettier-stylelint/config.js"
   ],
   ],
   "ignoreFiles": [
   "ignoreFiles": [
-    "src/client/styles/scss/_override-bootstrap-variables.scss",
-    "src/linter-checker/test.scss"
+    "src/client/styles/scss/_override-bootstrap-variables.scss"
   ],
   ],
   "rules": {
   "rules": {
     "indentation": 2,
     "indentation": 2,

+ 0 - 1
.vscode/extensions.json

@@ -12,7 +12,6 @@
     "christian-kohler.npm-intellisense",
     "christian-kohler.npm-intellisense",
     "esbenp.prettier-vscode",
     "esbenp.prettier-vscode",
     "shinnn.stylelint",
     "shinnn.stylelint",
-    "hex-ci.stylelint-plus",
 	],
 	],
 	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
 	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
 	"unwantedRecommendations": [
 	"unwantedRecommendations": [

+ 2 - 7
.vscode/launch.json

@@ -4,12 +4,6 @@
     // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
     // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "version": "0.2.0",
     "configurations": [
     "configurations": [
-      {
-        "type": "node",
-        "request": "attach",
-        "name": "Debug: Attach Debugger to Server",
-        "port": 9229
-      },
       {
       {
         "type": "node",
         "type": "node",
         "request": "launch",
         "request": "launch",
@@ -17,9 +11,10 @@
         "runtimeExecutable": "npm",
         "runtimeExecutable": "npm",
         "runtimeArgs": [
         "runtimeArgs": [
           "run",
           "run",
-          "server:nolazy"
+          "server:debug"
         ],
         ],
         "port": 9229,
         "port": 9229,
+        "timeout": 30000,
         "restart": true,
         "restart": true,
         "console": "integratedTerminal",
         "console": "integratedTerminal",
         "internalConsoleOptions": "neverOpen"
         "internalConsoleOptions": "neverOpen"

+ 6 - 15
.vscode/settings.json

@@ -6,24 +6,15 @@
     "javascript": "jsx"
     "javascript": "jsx"
   },
   },
 
 
-  // use stylelint-plus
-  // see https://qiita.com/y-w/items/bd7f11013fe34b69f0df#vs-code%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B
-  "css.validate": false,
-  "scss.validate": false,
-  "[css]": {
-    "editor.formatOnSave": true
-  },
-  "[scss]": {
-    "editor.formatOnSave": true
-  },
-  "stylelint.autoFixOnSave": true,
-
   // for vscode-eslint
   // for vscode-eslint
+  "eslint.autoFixOnSave": true,
   "[javascript]": {
   "[javascript]": {
     "editor.formatOnSave": false
     "editor.formatOnSave": false
   },
   },
-  "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": true,
-    "source.fixAll.markdownlint": true
+
+  // for prettier-vecode + prettier-stylelint
+  "prettier.stylelintIntegration": true,
+  "[scss]": {
+    "editor.formatOnSave": true
   }
   }
 }
 }

+ 11 - 134
CHANGES.md

@@ -1,128 +1,9 @@
 # CHANGES
 # CHANGES
 
 
-## v3.6.4-RC
+## 3.5.17-RC
 
 
-* Feature: Alert for stale page
-* Improvement: Reactify admin pages (App)
-* Improvement: Accessibility for editor icons of dark themes
-
-## v3.6.3
-
-* Improvement: Searching users in UserGroup Management
-* Fix: Repair google authentication by migrating to jaredhanson/passport-google-oauth2
-* Fix: Markdown Settings are broken by the button to import recommended settings
-* Support: Upgrade libs
-    * check-node-version
-    * file-loader
-    * mini-css-extract-plugin
-
-## 3.6.2
-
-* Improvement: Reactify admin pages (Customize)
-* Improvement: Ensure not to consider `[text|site](https://example.com]` as a row in the table
-* Improvement: Enter key behavior in markdown table
-* Fix: Pre-installed plugins in official docker image are not detected
-    * Introduced by 3.6.0
-* Fix: Emoji Autocomplete window does not float correctly
-    * Introduced by 3.5.0
-
-## 3.6.1
-
-### BREAKING CHANGES
-
-* 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))
-
-Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/36x.html>
-
-### Updates
-
-* Improvement: Drop unnecessary MongoDB collection indexes
-* Improvement: Accessibility of Antarctic theme
-* Improvement: Reactify admin pages (Markdown Settings)
-* 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
-
-## 3.5.23
-
-* Fix: Global Notification failed to send e-mail
-* Fix: Pagination is not working for trash list
-* Fix: Healthcheck API with `?connectToMiddlewares` returns error
-* Support: Upgrade libs
-    * growi-commons
-
-## 3.5.22
-
-* Improvement: Add `FILE_UPLOAD_DISABLED` env var
-
-## 3.5.21
-
-* Improvement: Cache control when retrieving attachment data
-* Fix: Inviting user doesn't work
-    * Introduced by 3.5.20
-
-## 3.5.20
-
-* Improvement: Organize MongoDB collection indexes uniqueness
-* Improvement: Reactify admin pages (External Account Management)
-* Fix: Search result or Timeline shows loading icon eternally when retrieving not accessible page
-* Support: Use SearchBox Elasticsearch Addon on Heroku
-* Support: Upgrade libs
-    * cross-env
-    * eslint-plugin-jest
-    * i18next
-    * i18next-browser-languagedetector
-    * migrate-mongo
-    * react-i18next
-    * validator
-
-## 3.5.19 (Missing number)
-
-## 3.5.18
-
-* Improvement: Import GROWI Archive
-    * Process asynchronously
-    * Collection configurations
-    * Selectable mode (insert/upsert/flush and insert)
-    * Safely mode settings for configs and users collections
-    * Show errors view
-* Improvement: Optimize handling promise of stream when exporting archive
-* Improvement: Optimize handling promise of stream when building indices
-* Improvement: Add link to [docs.growi.org](https://docs.growi.org)
-* Fix: Monospace font code is broken when printing on Mac
-
-## 3.5.17
-
-* Feature: Upload to GCS (Google Cloud Storage)
-* Feature: Statistics API
-* Improvement: Optimize exporting
-* Improvement: Show progress bar when exporting
-* Improvement: Validate collection combinations when importing
-* Improvement: Reactify admin pages
 * Fix: Use HTTP PlantUML URL in default
 * Fix: Use HTTP PlantUML URL in default
     * Introduced by 3.5.12
     * Introduced by 3.5.12
-* Fix: Config default values
-* Support: REPL with `console` npm scripts
 
 
 ## 3.5.16
 ## 3.5.16
 
 
@@ -264,7 +145,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/36x.html>
 * The restriction mode of the root page (`/`) will be set 'Public'
 * The restriction mode of the root page (`/`) will be set 'Public'
 * The restriction mode of the root page (`/`) can not be changed after v 3.5.1
 * The restriction mode of the root page (`/`) can not be changed after v 3.5.1
 
 
-Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/35x.html>
+Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
 
 
 ### Updates
 ### Updates
 
 
@@ -381,7 +262,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/35x.html>
 
 
 None.
 None.
 
 
-Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
+Upgrading Guide: https://docs.growi.org/guide/upgrading/34x.html
 
 
 ### Updates
 ### Updates
 
 
@@ -694,7 +575,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 * Improvement: Post comment with `Ctrl-Enter`
 * Improvement: Post comment with `Ctrl-Enter`
 * Improvement: Place the commented page at the beginning of the list
 * Improvement: Place the commented page at the beginning of the list
 * Improvement: Resolve errors on IE11 (Experimental)
 * Improvement: Resolve errors on IE11 (Experimental)
-* Support: Migrate to webpack 4
+* Support: Migrate to webpack 4 
 * Support: Upgrade libs
 * Support: Upgrade libs
     * eslint
     * eslint
     * react-bootstrap-typeahead
     * react-bootstrap-typeahead
@@ -718,7 +599,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 
 
 * Feature: Support [blockdiag](http://blockdiag.com)
 * Feature: Support [blockdiag](http://blockdiag.com)
 * Feature: Add `BLOCKDIAG_URI` environment variable
 * Feature: Add `BLOCKDIAG_URI` environment variable
-* Fix: Select modal for group is not shown
+* Fix: Select modal for group is not shown 
 * Support: Upgrade libs
 * Support: Upgrade libs
     * googleapis
     * googleapis
     * throttle-debounce
     * throttle-debounce
@@ -755,21 +636,17 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 * Improvement: Add 'future' theme
 * Improvement: Add 'future' theme
 * Improvement: Modify syntax for Crowi compatible template feature
 * Improvement: Modify syntax for Crowi compatible template feature
     * *before*
     * *before*
-
-        ~~~markdown
+        ~~~
         ``` template:/page/name
         ``` template:/page/name
         page contents
         page contents
         ```
         ```
         ~~~
         ~~~
-
     * *after*
     * *after*
-
-        ~~~plane
+        ~~~
         ::: template:/page/name
         ::: template:/page/name
         page contents
         page contents
         :::
         :::
         ~~~
         ~~~
-
 * Improvement: Escape iframe tag in block codes
 * Improvement: Escape iframe tag in block codes
 * Support: Upgrade libs
 * Support: Upgrade libs
     * assets-webpack-plugin
     * assets-webpack-plugin
@@ -792,7 +669,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 * Improvement: Auto-format markdown table which includes multibyte text
 * Improvement: Auto-format markdown table which includes multibyte text
 * Improvement: Show icon when auto-format markdown table is activated
 * Improvement: Show icon when auto-format markdown table is activated
 * Improvement: Enable to switch show/hide border for highlight.js
 * Improvement: Enable to switch show/hide border for highlight.js
-* Improvement: BindDN field allows also ActiveDirectory styles
+* Improvement: BindDN field allows also ActiveDirectory styles 
 * Improvement: Show LDAP logs when testing login
 * Improvement: Show LDAP logs when testing login
 * Fix: Comment body doesn't break long terms
 * Fix: Comment body doesn't break long terms
 * Fix: lsx plugin lists up pages that hit by forward match wrongly
 * Fix: lsx plugin lists up pages that hit by forward match wrongly
@@ -944,7 +821,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 * Support: Upgrade libs
 * Support: Upgrade libs
     * uglifycss
     * uglifycss
     * sinon-chai
     * sinon-chai
-
+    
 ## 2.4.2
 ## 2.4.2
 
 
 * Improvement: Ensure to set absolute url from root when attaching files when `FILE_UPLOAD=local`
 * Improvement: Ensure to set absolute url from root when attaching files when `FILE_UPLOAD=local`
@@ -1164,7 +1041,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 
 
 ## 1.2.13
 ## 1.2.13
 
 
-* Improvement: Enabled to switch whether to push states with History API when tabs changes
+* Improvement: Enabled to switch whether to push states with History API when tabs changes 
 * Fix: Layout of the Not Found page
 * Fix: Layout of the Not Found page
 
 
 ## 1.2.12 (Missing number)
 ## 1.2.12 (Missing number)
@@ -1325,7 +1202,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 
 
 ## 1.0.2
 ## 1.0.2
 
 
-* Improvement: For lsx
+* Improvement: For lsx 
 
 
 ## 1.0.1
 ## 1.0.1
 
 

+ 148 - 48
README.md

@@ -16,95 +16,108 @@
 </p>
 </p>
 
 
 
 
-GROWI
+GROWI 
 ===========
 ===========
 
 
-[![Actions Status](https://github.com/weseek/growi/workflows/Node%20CI/badge.svg)](https://github.com/weseek/growi/actions)
+[![wercker status](https://app.wercker.com/status/595b761d0e26796ddb304679f7bf27de/s/master "wercker status")](https://app.wercker.com/project/byKey/595b761d0e26796ddb304679f7bf27de)
 [![dependencies status](https://david-dm.org/weseek/growi.svg)](https://david-dm.org/weseek/growi)
 [![dependencies status](https://david-dm.org/weseek/growi.svg)](https://david-dm.org/weseek/growi)
 [![devDependencies Status](https://david-dm.org/weseek/growi/dev-status.svg)](https://david-dm.org/weseek/growi?type=dev)
 [![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/)
 [![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)
 - [Features](#features)
 - [Quick Start for Production](#quick-start-for-production)
 - [Quick Start for Production](#quick-start-for-production)
-    - [Heroku](#heroku)
-    - [docker-compose](#docker-compose)
     - [On-premise](#on-premise)
     - [On-premise](#on-premise)
+    - [Using Heroku](#using-heroku)
+    - [Using docker-compose](#using-docker-compose)
 - [Environment Variables](#environment-variables)
 - [Environment Variables](#environment-variables)
 - [Documentation](#documentation)
 - [Documentation](#documentation)
 - [License](#license)
 - [License](#license)
 
 
+
 Features
 Features
 ========
 ========
 
 
-* **Features**
-    * Create hierarchical pages with markdown -> [HERE](https://docs.growi.org/en/guide/getting-started/five_minutes.html) is 5 minutes tutorial
-    * Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
-        * [GROWI Docs: HackMD(CodiMD) Integration](https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html)
-    * Support Authentication with LDAP / Active Directory, OAuth
-    * SSO(Single Sign On) with SAML
-    * Slack/Mattermost, IFTTT Integration
-    * [GROWI Docs: Features](https://docs.growi.org/en/guide/features/page_layout.html)
 * **Pluggable**
 * **Pluggable**
-    * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
+  * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
+* **Features**
+  * Create hierarchical pages with markdown
+  * Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
+  * Support Authentication with LDAP / Active Directory, OAuth
+  * SSO(Single Sign On) with SAML
+  * Slack/Mattermost, IFTTT Integration
+  * [Miscellaneous features](https://github.com/weseek/growi/wiki/Additional-Features)
 * **[Docker Ready][dockerhub]**
 * **[Docker Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
 * **[Docker Compose Ready][docker-compose]**
-    * [GROWI Docs: Multiple sites](https://docs.growi.org/en/admin-guide/admin-cookbook/multi-app.html)
-    * [GROWI Docs: HTTPS(with Let's Encrypt) proxy integration](https://docs.growi.org/en/admin-guide/admin-cookbook/lets-encrypt.html)
+  * [Multiple sites example](https://github.com/weseek/growi-docker-compose/tree/master/examples/multi-app)
+  * [HTTPS(with Let's Encrypt) proxy integration example](https://github.com/weseek/growi-docker-compose/tree/master/examples/https-portal)
+* Support IE11 (Experimental)
 
 
 Quick Start for Production
 Quick Start for Production
 ===========================
 ===========================
 
 
-### Heroku
+Using Heroku
+------------
 
 
-- [GROWI Docs: Launch on Heroku](https://docs.growi.org/en/admin-guide/getting-started/heroku.html) ([en](https://docs.growi.org/en/admin-guide/getting-started/heroku.html)/[ja](https://docs.growi.org/ja/admin-guide/getting-started/heroku.html))
+1. Go to https://heroku.com/deploy
+2. (Optional) Input INSTALL_PLUGINS to install plugins
 
 
-### docker-compose
+Using docker-compose
+---------------------
 
 
-- [GROWI Docs: Launch with docker-compose](https://docs.growi.org/en/admin-guide/getting-started/docker-compose.html) ([en](https://docs.growi.org/en/admin-guide/getting-started/docker-compose.html)/[ja](https://docs.growi.org/ja/admin-guide/getting-started/docker-compose.html))
+```bash
+git clone https://github.com/weseek/growi-docker-compose.git growi
+cd growi
+docker-compose up
+```
 
 
-### On-premise
+See also [weseek/growi-docker-compose][docker-compose]
 
 
-**[Migration Guide from Crowi](https://docs.growi.org/en/admin-guide/migration-guide/from-crowi-onpremise.html) ([en](https://docs.growi.org/en/admin-guide/migration-guide/from-crowi-onpremise.html)/[ja](https://docs.growi.org/ja/admin-guide/migration-guide/from-crowi-onpremise.html))** is here.
+On-premise
+----------
 
 
-- [GROWI Docs: Install on Ubuntu Server](https://docs.growi.org/en/admin-guide/getting-started/ubuntu-server.html)
-- [GROWI Docs: Install on CentOS](https://docs.growi.org/en/admin-guide/getting-started/centos.html)
+[**Migration Guide from Crowi** is here](https://docs.growi.org/guide/migration-guide/from-crowi-onpremise.html).
 
 
+### Dependencies
 
 
-Configuration
-============
+- Node.js v10.x (DON'T USE 11.x)
+- npm 6.x
+- yarn
+- MongoDB 3.x
 
 
-See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](https://docs.growi.org/en/admin-guide/)/[ja](https://docs.growi.org/ja/admin-guide/)).
+See [confirmed versions](https://docs.growi.org/dev/startup/dev-env.html#versions-confirmed-to-work).
 
 
-## Environment Variables
+#### Optional Dependencies
 
 
-- [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
+- Redis 3.x
+- ElasticSearch 6.x (needed when using Full-text search)
+  - **CAUTION: Following plugins are required**
+      - [Japanese (kuromoji) Analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
+      - [ICU Analysis Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html)
 
 
+### How to start
 
 
-Development
-==========
+#### Build and run the app
 
 
-## Dependencies
+```bash
+git clone https://github.com/weseek/growi.git
+cd growi
+yarn
+MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi npm start
+```
 
 
-- Node.js v10.x (DON'T USE 11.x)
-- npm 6.x
-- yarn
-- MongoDB 3.x
+**DO NOT USE `npm install`**, use `yarn` instead.
 
 
-See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-up-node-js-environment).
+If you launch growi with ElasticSearch, add environment variables before `npm start` like following:
 
 
-### Optional Dependencies
+```
+export MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi
+export ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi
+npm start
+```
 
 
-- Redis 3.x
-- ElasticSearch 6.x (needed when using Full-text search)
-    - **CAUTION: Following plugins are required**
-        - [Japanese (kuromoji) Analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
-        - [ICU Analysis Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html)
+For more info, see [Developers Guide](https://docs.growi.org/dev/).
 
 
-## Command details
+#### Command details
 
 
 |command|desc|
 |command|desc|
 |--|--|
 |--|--|
@@ -112,7 +125,84 @@ See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-
 |`npm run server:prod`|Launch the server|
 |`npm run server:prod`|Launch the server|
 |`npm start`|Invoke `npm run build:prod` and `npm run server:prod`|
 |`npm start`|Invoke `npm run build:prod` and `npm run server:prod`|
 
 
-For more info, see [GROWI Docs: List of npm Commands](https://docs.growi.org/en/dev/startup/launch.html#list-of-npm-commands).
+### How to upgrade
+
+```bash
+git pull
+yarn
+npm start
+```
+
+### How to install plugins
+
+* Stop server if server is running
+* `yarn add` to install plugin or `npm install`
+* `npm start` to build client app and start server
+
+#### Examples
+
+```bash
+yarn add growi-plugin-lsx
+npm start
+```
+
+For more info, see [Developers Guide](https://docs.growi.org/dev/) on docs.growi.org.
+
+
+Environment Variables
+======================
+
+* **Required**
+    * MONGO_URI: URI to connect to MongoDB.
+* **Option**
+    * NODE_ENV: `production` OR `development`.
+    * PORT: Server port. default: `3000`.
+    * NO_CDN: If `true`, system doesn't use CDN, all resources will be downloaded from CDN when build client, and served by the GROWI Express server. default: `false`.
+    * ELASTICSEARCH_URI: URI to connect to Elasticearch.
+    * REDIS_URI: URI to connect to Redis (use it as a session store instead of MongoDB).
+    * PASSWORD_SEED: A password seed used by password hash generator.
+    * SECRET_TOKEN: A secret key for verifying the integrity of signed cookies.
+    * SESSION_NAME: The name of the session ID cookie to set in the response by Express. default: `connect.sid`
+    * FILE_UPLOAD: Attached files storage. default: `aws`
+      * `aws` : AWS S3 (needs AWS settings on Admin page)
+      * `mongodb` : MongoDB GridFS (Setting-less)
+      * `local` : Server's Local file system (Setting-less)
+      * `none` : Disable file uploading
+    * MAX_FILE_SIZE: The maximum file size limit for uploads (bytes). default: `Infinity`
+    * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
+    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: If `true`, the system uses only the value of the environment variable as the value of the SAML option that can be set via the environment variable.
+    * PUBLISH_OPEN_API: Publish GROWI OpenAPI resources with [ReDoc](https://github.com/Rebilly/ReDoc). Visit `/api-docs`.
+    * FORCE_WIKI_MODE: Forces wiki mode. default: undefined
+      * `public`  : Forces all pages to become public
+      * `private` : Forces all pages to become private
+      * undefined : Publicity will be configured by the admin security page settings
+    * FORMAT_NODE_LOG: If `false`, Output server log as JSON. defautl: `true` (Enabled only when `NODE_ENV=production`)
+* **Option to integrate with external systems**
+    * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
+        * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html).**
+    * HACKMD_URI_FOR_SERVER: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server from GROWI Express server. If not set, `HACKMD_URI` will be used.
+    * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
+    * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.
+* **Option (Overwritable in admin page)**
+    * APP_SITE_URL: Site URL. e.g. `https://example.com`, `https://example.com:8080`
+    * LOCAL_STRATEGY_ENABLED: Enable or disable ID/Pass login
+    * LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some ID/Pass login options
+    * SAML_ENABLED: Enable or disable SAML
+    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some SAML options
+    * SAML_ENTRY_POINT: IdP entry point
+    * SAML_ISSUER: Issuer string to supply to IdP
+    * SAML_ATTR_MAPPING_ID: Attribute map for id
+    * SAML_ATTR_MAPPING_USERNAME: Attribute map for username
+    * SAML_ATTR_MAPPING_MAIL: Attribute map for email
+    * SAML_ATTR_MAPPING_FIRST_NAME: Attribute map for first name
+    * SAML_ATTR_MAPPING_LAST_NAME:  Attribute map for last name
+    * SAML_CERT: PEM-encoded X.509 signing certificate string to validate the response from IdP
+    * OAUTH_GOOGLE_CLIENT_ID: Google API client id for OAuth login.
+    * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
+    * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
+    * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_KEY: Twitter consumer key(API key) for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_SECRET: Twitter consumer secret(API secret) for OAuth login.
 
 
 
 
 Documentation
 Documentation
@@ -142,6 +232,16 @@ Repository. If you would like to *implement* a new feature, firstly please submi
 It also allows us to coordinate better, prevent duplication of work and help you to create the change so it can be successfully accepted into the project.
 It also allows us to coordinate better, prevent duplication of work and help you to create the change so it can be successfully accepted into the project.
 * **Small Features** can be created and directly [submitted as a Pull Request][pulls].
 * **Small Features** can be created and directly [submitted as a Pull Request][pulls].
 
 
+Translation
+--------------
+
+We have some Transifex Projects.
+
+* [GROWI (Internationalize)](https://www.transifex.com/weseek-inc/growi)
+* [GROWI Docs (Internationalize)](https://www.transifex.com/weseek-inc/growi-docs)
+
+Please join to our team!
+
 
 
 Language on GitHub
 Language on GitHub
 ------------------
 ------------------

+ 5 - 5
app.json

@@ -25,18 +25,18 @@
       "description": "A password seed is used by password hash generator. ",
       "description": "A password seed is used by password hash generator. ",
       "generator": "secret"
       "generator": "secret"
     },
     },
-    "ADDITIONAL_PACKAGES": {
-      "description": "Space-separated list of npm package names to install.",
-      "value": "growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion",
+    "INSTALL_PLUGINS": {
+      "description": "Comma-separated list of plugin package names to install.",
+      "value": "growi-plugin-lsx,growi-plugin-pukiwiki-like-linker",
       "required": false
       "required": false
     }
     }
   },
   },
   "addons": [
   "addons": [
     "mongolab",
     "mongolab",
     {
     {
-      "plan": "searchbox:starter",
+      "plan": "bonsai:sandbox-6",
       "options": {
       "options": {
-        "es_version": "6"
+        "version": "6.5.4"
       }
       }
     }
     }
   ]
   ]

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

@@ -1,6 +0,0 @@
-#!/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 - 3
bin/heroku/install-packages.sh

@@ -1,3 +0,0 @@
-#!/bin/sh
-
-yarn add $ADDITIONAL_PACKAGES

+ 7 - 0
bin/heroku/install-plugins.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+export IFS=","
+
+for plugin in $INSTALL_PLUGINS; do
+  yarn add $plugin
+done

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

@@ -0,0 +1,9 @@
+#!/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

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

@@ -0,0 +1,40 @@
+#!/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

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

@@ -0,0 +1,28 @@
+#!/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

+ 8 - 10
config/jest.config.js

@@ -1,15 +1,6 @@
 // For a detailed explanation regarding each configuration property, visit:
 // For a detailed explanation regarding each configuration property, visit:
 // https://jestjs.io/docs/en/configuration.html
 // https://jestjs.io/docs/en/configuration.html
 
 
-const MODULE_NAME_MAPPING = {
-  '@root/(.+)': '<rootDir>/$1',
-  '@commons/(.+)': '<rootDir>/src/lib/$1',
-  '@server/(.+)': '<rootDir>/src/server/$1',
-  '@alias/logger': '<rootDir>/src/lib/service/logger',
-  // -- doesn't work with unknown error -- 2019.06.19 Yuki Takei
-  // debug: '<rootDir>/src/lib/service/logger/alias-for-debug',
-};
-
 module.exports = {
 module.exports = {
   // Indicates whether each individual test should be reported during the run
   // Indicates whether each individual test should be reported during the run
   verbose: true,
   verbose: true,
@@ -28,7 +19,14 @@ module.exports = {
       // Automatically clear mock calls and instances between every test
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       clearMocks: true,
       // A map from regular expressions to module names that allow to stub out resources with a single module
       // A map from regular expressions to module names that allow to stub out resources with a single module
-      moduleNameMapper: MODULE_NAME_MAPPING,
+      moduleNameMapper: {
+        '@root/(.+)': '<rootDir>/$1',
+        '@commons/(.+)': '<rootDir>/src/lib/$1',
+        '@server/(.+)': '<rootDir>/src/server/$1',
+        '@alias/logger': '<rootDir>/src/lib/service/logger',
+        // -- doesn't work with unknown error -- 2019.06.19 Yuki Takei
+        // debug: '<rootDir>/src/lib/service/logger/alias-for-debug',
+      },
     },
     },
     // {
     // {
     //   displayName: 'client',
     //   displayName: 'client',

+ 16 - 16
config/migrate.js

@@ -7,26 +7,26 @@
 
 
 require('module-alias/register');
 require('module-alias/register');
 
 
-const { URL } = require('url');
+function getMongoUri(env) {
+  return env.MONGOLAB_URI // for B.C.
+    || env.MONGODB_URI // MONGOLAB changes their env name
+    || env.MONGOHQ_URL
+    || env.MONGO_URI
+    || ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
+}
 
 
-const { getMongoUri } = require('@commons/util/mongoose-utils');
-
-const mongoUri = getMongoUri();
-
-// parse url
-const url = new URL(mongoUri);
-
-const mongodb = {
-  url: mongoUri,
-  databaseName: url.pathname.substring(1), // omit heading slash
-  options: {
-    useNewUrlParser: true, // removes a deprecation warning when connecting
-  },
-};
+const mongoUri = getMongoUri(process.env);
+const match = mongoUri.match(/^(.+)\/([^/]+)$/);
 
 
 module.exports = {
 module.exports = {
   mongoUri,
   mongoUri,
-  mongodb,
+  mongodb: {
+    url: match[0],
+    databaseName: match[2],
+    options: {
+      useNewUrlParser: true, // removes a deprecation warning when connecting
+    },
+  },
   migrationsDir: 'src/migrations/',
   migrationsDir: 'src/migrations/',
   changelogCollectionName: 'migrations',
   changelogCollectionName: 'migrations',
 };
 };

+ 2 - 4
config/swagger-definition.js

@@ -1,16 +1,14 @@
 const pkg = require('../package.json');
 const pkg = require('../package.json');
 
 
-const apiVersion = process.env.API_VERSION || 3;
-
 module.exports = {
 module.exports = {
   openapi: '3.0.1',
   openapi: '3.0.1',
   info: {
   info: {
-    title: `GROWI REST API v${apiVersion}`,
+    title: 'GROWI REST API v3',
     version: pkg.version,
     version: pkg.version,
   },
   },
   servers: [
   servers: [
     {
     {
-      url: 'https://demo.growi.org',
+      url: 'https://demo.growi.org/_api/v3/',
     },
     },
   ],
   ],
 };
 };

+ 0 - 9
config/webpack.common.js

@@ -8,7 +8,6 @@ const webpack = require('webpack');
  */
  */
 const WebpackAssetsManifest = require('webpack-assets-manifest');
 const WebpackAssetsManifest = require('webpack-assets-manifest');
 const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
 const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const helpers = require('../src/lib/util/helpers');
 const helpers = require('../src/lib/util/helpers');
 
 
 /*
 /*
@@ -128,14 +127,6 @@ module.exports = (options) => {
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
       new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
       new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
 
 
-      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 LodashModuleReplacementPlugin({
       new LodashModuleReplacementPlugin({
         flattening: true,
         flattening: true,
       }),
       }),

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

@@ -0,0 +1,49 @@
+/**
+ * @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',
+    }),
+  ],
+};

+ 7 - 0
config/webpack.dev.js

@@ -2,6 +2,8 @@
  * @author: Yuki Takei <yuki@weseek.co.jp>
  * @author: Yuki Takei <yuki@weseek.co.jp>
  */
  */
 
 
+const webpack = require('webpack');
+
 /*
 /*
  * Webpack Plugins
  * Webpack Plugins
  */
  */
@@ -57,6 +59,11 @@ module.exports = require('./webpack.common')({
       filename: '[name].bundle.css',
       filename: '[name].bundle.css',
     }),
     }),
 
 
+    new webpack.DllReferencePlugin({
+      context: helpers.root(),
+      manifest: require(helpers.root('public/dll', 'manifest.json')),
+    }),
+
     new BundleAnalyzerPlugin({
     new BundleAnalyzerPlugin({
       analyzerMode: ANALYZE ? 'server' : 'disabled',
       analyzerMode: ANALYZE ? 'server' : 'disabled',
     }),
     }),

+ 0 - 118
docker/Dockerfile

@@ -1,118 +0,0 @@
-# syntax = docker/dockerfile:experimental
-
-ARG flavor=default
-
-
-
-##
-## deps-resolver
-##
-FROM node:12-slim AS deps-resolver
-LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
-
-ENV appDir /opt/growi
-
-COPY ./package.json ${appDir}/
-COPY ./yarn.lock ${appDir}/
-WORKDIR ${appDir}
-
-# setup
-RUN yarn config set network-timeout 300000
-RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
-  yarn
-# install official plugins
-RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
-  yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-# install peerDependencies
-RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
-  yarn add -D react-images@1.0.0 react-motion
-
-
-
-##
-## deps-resolver-prod
-##
-FROM deps-resolver AS deps-resolver-prod
-
-# shrink dependencies for production
-RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
-  yarn install --production
-
-
-
-##
-## prebuilder-default
-##
-FROM node:12-slim AS prebuilder-default
-LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
-
-ENV appDir /opt/growi
-
-COPY --from=deps-resolver ${appDir}/node_modules ${appDir}/node_modules
-
-# copy all files except the files listed in Dockerfile.dockerignore
-COPY . ${appDir}
-
-# overwirte package.json and yarn.lock
-COPY --from=deps-resolver ${appDir}/package.json ${appDir}/package.json
-COPY --from=deps-resolver ${appDir}/yarn.lock ${appDir}/yarn.lock
-
-
-
-##
-## prebuilder-nocdn
-##
-FROM prebuilder-default AS prebuilder-nocdn
-
-# replace env.prod.js for NO_CDN
-COPY docker/nocdn/env.prod.js ${appDir}/config/
-
-
-
-##
-## builder
-##
-FROM prebuilder-${flavor} AS builder
-
-ENV appDir /opt/growi
-
-WORKDIR ${appDir}
-
-# build
-RUN --mount=type=cache,target=./node_modules/.cache \
-  yarn build:prod
-
-# remove except artifacts
-WORKDIR /tmp
-RUN --mount=target=. sh docker/bin/remove-except-artifacts.sh
-WORKDIR ${appDir}
-
-
-
-##
-## release
-##
-FROM node:12-alpine
-LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
-
-ENV appDir /opt/growi
-
-# install tini
-RUN --mount=type=cache,target=/var/cache/apk \
-  apk add tini su-exec
-
-COPY docker/docker-entrypoint.sh /
-RUN chmod 700 /docker-entrypoint.sh
-
-COPY --from=deps-resolver-prod --chown=node:node \
-  ${appDir}/node_modules ${appDir}/node_modules
-COPY --from=builder --chown=node:node \
-  ${appDir} ${appDir}
-
-WORKDIR ${appDir}
-
-VOLUME /data
-EXPOSE 3000
-
-ENTRYPOINT ["/sbin/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
-CMD ["yarn", "server:prod"]

+ 0 - 13
docker/Dockerfile.dockerignore

@@ -1,13 +0,0 @@
-.git
-.github
-.vscode
-node_modules
-src/linter-checker
-src/test
-.editorconfig
-.eslint*
-.gitignore
-.prettier*
-.stylelint*
-app.json
-Procfile

+ 0 - 93
docker/README.md

@@ -1,93 +0,0 @@
-
-GROWI Official docker image
-========================
-
-[![Actions Status](https://github.com/weseek/growi/workflows/Release%20Docker%20Images/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
-
-![GROWI-x-docker](https://user-images.githubusercontent.com/1638767/38307565-105956e2-384f-11e8-8534-b1128522d68d.png)
-
-
-Supported tags and respective Dockerfile links
-------------------------------------------------
-
-* [`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)
-
-
-What is GROWI?
--------------
-
-GROWI is a team collaboration software and it forked from [crowi](https://github.com/weseek/crowi/crowi)
-
-see: [weseek/growi](https://github.com/weseek/growi)
-
-What is growi-docker?
--------------------
-
-The GROWI official docker image for production use which concludes several official plugins.
-
-- [growi-plugin-lsx](https://www.npmjs.com/package/growi-plugin-lsx)
-- [growi-plugin-pukiwiki-like-linker](https://www.npmjs.com/package/growi-plugin-pukiwiki-like-linker)
-- [growi-plugin-attachment-refs](https://www.npmjs.com/package/growi-plugin-attachment-refs)
-
-
-
-Requirements
--------------
-
-* MongoDB (>= 3.6)
-
-### Optional Dependencies
-
-* ElasticSearch (>= 6.6)
-    * Japanese (kuromoji) Analysis plugin
-    * ICU Analysis Plugin
-
-
-Usage
------
-
-```bash
-docker run -d \
-    -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
-    weseek/growi
-```
-
-and go to `http://localhost:3000/` .
-
-If you use ElasticSearch, type this:
-
-```bash
-docker run -d \
-    -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
-    -e ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi \
-    weseek/growi
-```
-
-
-### docker-compose
-
-Using docker-compose is the fastest and the most convenient way to boot GROWI.
-
-see: [weseek/growi-docker-compose](https://github.com/weseek/growi-docker-compose)
-
-
-Configuration
------------
-
-See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](https://docs.growi.org/en/admin-guide/)/[ja](https://docs.growi.org/ja/admin-guide/)).
-
-### Environment Variables
-
-- [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
-
-
-Issues
-------
-
-If you have any issues or questions about this image, please contact us through  [GitHub issue](https://github.com/weseek/growi-docker/issues).
-

+ 0 - 7
docker/bin/remove-except-artifacts.sh

@@ -1,7 +0,0 @@
-#!/bin/sh
-
-set -e
-
-rm -rf \
-  ${appDir}/docker \
-  ${appDir}/node_modules \

+ 0 - 14
docker/docker-entrypoint.sh

@@ -1,14 +0,0 @@
-#!/bin/sh
-
-set -e
-
-# Support `FILE_UPLOAD=local`
-mkdir -p /data/uploads
-if [ ! -e "$appDir/public/uploads" ]; then
-  ln -s /data/uploads $appDir/public/uploads
-fi
-
-chown node:node /data/uploads
-chown -h node:node $appDir/public/uploads
-
-su-exec node $@

+ 0 - 5
docker/nocdn/env.prod.js

@@ -1,5 +0,0 @@
-module.exports = {
-  NODE_ENV: 'production',
-  NO_CDN: true,
-  // FORMAT_NODE_LOG: false,
-};

+ 26 - 34
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.6.4-RC",
+  "version": "3.5.17-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -20,24 +20,22 @@
     "url": "https://github.com/weseek/growi/issues"
     "url": "https://github.com/weseek/growi/issues"
   },
   },
   "scripts": {
   "scripts": {
-    "build:api:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js \"src/server/**/*.js\"",
-    "build:apiv3:jsdoc": "cross-env API_VERSION=3 npm run build:api:jsdoc",
-    "build:apiv1:jsdoc": "cross-env API_VERSION=1 npm run build:api:jsdoc",
+    "build:apiv3:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js src/server/routes/apiv3/**/*.js",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "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:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
-    "build:dev:watch": "npm run build:dev:app:watch",
-    "build:dev": "npm run build:dev:app",
+    "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:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "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:prod": "env-cmd -f config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
     "build": "npm run build:dev:watch",
     "build": "npm run build:dev:watch",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:report": "rimraf -- report",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "clean": "npm-run-all -p clean:*",
-    "console": "env-cmd -f config/env.dev.js node --experimental-repl-await src/server/console.js",
-    "heroku-postbuild": "sh bin/heroku/install-packages.sh && npm run build:prod",
+    "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "lint:js:fix": "eslint \"**/*.{js,jsx}\" --fix",
     "lint:js:fix": "eslint \"**/*.{js,jsx}\" --fix",
     "lint:js": "eslint \"**/*.{js,jsx}\"",
     "lint:js": "eslint \"**/*.{js,jsx}\"",
-    "lint:styles:fix": "stylelint --fix src/client/styles/scss/**/*.scss",
+    "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint:swagger2openapi": "node node_modules/swagger2openapi/oas-validate tmp/swagger.json",
     "lint:swagger2openapi": "node node_modules/swagger2openapi/oas-validate tmp/swagger.json",
     "lint": "npm-run-all -p lint:js lint:styles lint:swagger2openapi",
     "lint": "npm-run-all -p lint:js lint:styles lint:swagger2openapi",
@@ -54,8 +52,8 @@
     "preserver:prod": "npm run migrate",
     "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "prestart": "npm run build:prod",
     "resource": "node bin/download-cdn-resources.js",
     "resource": "node bin/download-cdn-resources.js",
-    "server:nolazy": "env-cmd -f config/env.dev.js node-dev --nolazy --inspect src/server/app.js",
-    "server:dev": "env-cmd -f config/env.dev.js node-dev --inspect src/server/app.js",
+    "server:debug": "env-cmd -f config/env.dev.js node-dev --inspect src/server/app.js",
+    "server:dev": "env-cmd -f config/env.dev.js node-dev --respawn src/server/app.js",
     "server:prod:ci": "npm run server:prod -- --ci",
     "server:prod:ci": "npm run server:prod -- --ci",
     "server:prod": "env-cmd -f config/env.prod.js node src/server/app.js",
     "server:prod": "env-cmd -f config/env.prod.js node src/server/app.js",
     "server": "npm run server:dev",
     "server": "npm run server:dev",
@@ -70,22 +68,20 @@
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0",
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0",
       "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
       "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
     ],
     ],
-    "@google-cloud/storage": "^3.3.0",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",
     "archiver": "^3.1.1",
     "archiver": "^3.1.1",
-    "array.prototype.flatmap": "^1.2.2",
     "async": "^3.0.1",
     "async": "^3.0.1",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",
     "axios": "^0.19.0",
     "axios": "^0.19.0",
     "body-parser": "^1.18.2",
     "body-parser": "^1.18.2",
     "bunyan": "^1.8.12",
     "bunyan": "^1.8.12",
     "bunyan-format": "^0.2.1",
     "bunyan-format": "^0.2.1",
-    "check-node-version": "^4.0.2",
+    "check-node-version": "=3.3.0",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^3.0.0",
     "connect-mongo": "^3.0.0",
     "connect-redis": "^3.3.0",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cookie-parser": "^1.4.3",
-    "cross-env": "^6.0.3",
+    "cross-env": "^5.0.5",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
     "date-fns": "^2.0.0",
     "diff": "^4.0.1",
     "diff": "^4.0.1",
@@ -102,21 +98,20 @@
     "express-validator": "^6.1.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^4.0.8",
+    "growi-commons": "^4.0.7",
     "helmet": "^3.13.0",
     "helmet": "^3.13.0",
-    "i18next": "^19.0.0",
+    "i18next": "^17.0.3",
     "i18next-express-middleware": "^1.4.1",
     "i18next-express-middleware": "^1.4.1",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
-    "is-iso-date": "^0.0.1",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
-    "migrate-mongo": "^7.0.1",
+    "migrate-mongo": "^6.0.0",
     "mkdirp": "~0.5.1",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
     "module-alias": "^2.0.6",
     "mongoose": "5.4.4",
     "mongoose": "5.4.4",
     "mongoose-gridfs": "^1.2.2",
     "mongoose-gridfs": "^1.2.2",
-    "mongoose-paginate-v2": "^1.3.2",
+    "mongoose-paginate": "^5.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
@@ -124,10 +119,9 @@
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
     "npm-run-all": "^4.1.2",
     "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
-    "package-installed-version-sync": "^2.1.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
-    "passport-google-oauth20": "^2.0.0",
+    "passport-google-auth": "^1.0.2",
     "passport-http": "^0.3.0",
     "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
@@ -143,7 +137,6 @@
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "url-join": "^4.0.0",
-    "validator": "^12.0.0",
     "xss": "^1.0.6"
     "xss": "^1.0.6"
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -178,14 +171,14 @@
     "eslint": "^6.0.1",
     "eslint": "^6.0.1",
     "eslint-config-weseek": "^1.0.3",
     "eslint-config-weseek": "^1.0.3",
     "eslint-plugin-import": "^2.18.0",
     "eslint-plugin-import": "^2.18.0",
-    "eslint-plugin-jest": "^23.0.3",
+    "eslint-plugin-jest": "^22.7.1",
     "eslint-plugin-react": "^7.14.2",
     "eslint-plugin-react": "^7.14.2",
-    "file-loader": "^5.0.2",
+    "file-loader": "^4.0.0",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
-    "hard-source-webpack-plugin": "^0.13.1",
-    "i18next-browser-languagedetector": "^4.0.1",
+    "i18next-browser-languagedetector": "^3.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
     "jest": "^24.8.0",
     "jest": "^24.8.0",
+    "jest-each": "^24.8.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
@@ -202,7 +195,7 @@
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "markdown-table": "^1.1.1",
     "metismenu": "^3.0.3",
     "metismenu": "^3.0.3",
-    "mini-css-extract-plugin": "^0.9.0",
+    "mini-css-extract-plugin": "^0.8.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
     "node-dev": "^4.0.0",
     "node-sass": "^4.12.0",
     "node-sass": "^4.12.0",
@@ -213,7 +206,7 @@
     "penpal": "^4.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
     "postcss-loader": "^3.0.0",
-    "prettier": "^1.19.1",
+    "prettier-stylelint": "^0.4.2",
     "react": "^16.8.3",
     "react": "^16.8.3",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.4.2",
     "react-bootstrap-typeahead": "^3.4.2",
@@ -223,7 +216,7 @@
     "react-dropzone": "^10.1.3",
     "react-dropzone": "^10.1.3",
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
-    "react-i18next": "^11.1.0",
+    "react-i18next": "^10.6.1",
     "react-waypoint": "^9.0.0",
     "react-waypoint": "^9.0.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
@@ -231,7 +224,6 @@
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^2.0.3",
     "socket.io-client": "^2.0.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
-    "stylelint": "^12.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
@@ -253,8 +245,8 @@
     "debug": "src/lib/service/logger/alias-for-debug"
     "debug": "src/lib/service/logger/alias-for-debug"
   },
   },
   "engines": {
   "engines": {
-    "node": ">=10.17.0 <13",
-    "npm": ">=6.11.3 <7",
-    "yarn": ">=1.19.1 <2"
+    "node": ">=8.11.1 <11",
+    "npm": ">=5.6.0 <7",
+    "yarn": ">=1.5.1 <2"
   }
   }
 }
 }

+ 90 - 138
resource/locales/en-US/translation.json

@@ -21,18 +21,20 @@
   "New": "New",
   "New": "New",
   "Shortcuts": "Shortcuts",
   "Shortcuts": "Shortcuts",
   "eg": "e.g.",
   "eg": "e.g.",
-  "add": "Add",
   "Undo": "Undo",
   "Undo": "Undo",
   "Article": "Article",
   "Article": "Article",
   "Page": "Page",
   "Page": "Page",
   "Page Path": "Page Path",
   "Page Path": "Page Path",
   "Category": "Category",
   "Category": "Category",
   "User": "User",
   "User": "User",
-  "status": "Status",
+  "status":"Status",
   "account_id": "Account Id",
   "account_id": "Account Id",
+
+
   "Update": "Update",
   "Update": "Update",
   "Update Page": "Update Page",
   "Update Page": "Update Page",
   "Warning": "Warning",
   "Warning": "Warning",
+
   "Sign in": "Sign in",
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
   "Sign up is here": "Sign up",
   "Sign in is here": "Sign in",
   "Sign in is here": "Sign in",
@@ -42,23 +44,29 @@
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Example": "Example",
   "Example": "Example",
   "Taro Yamada": "John Doe",
   "Taro Yamada": "John Doe",
+
   "List View": "List",
   "List View": "List",
   "Timeline View": "Timeline",
   "Timeline View": "Timeline",
   "History": "History",
   "History": "History",
   "Presentation Mode": "Presentation",
   "Presentation Mode": "Presentation",
-  "username": "Username",
+
   "Created": "Created",
   "Created": "Created",
   "Last updated": "Updated",
   "Last updated": "Updated",
   "Last_Login": "Last Login",
   "Last_Login": "Last Login",
+
   "Share": "Share",
   "Share": "Share",
   "Share Link": "Share Link",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Markdown Link": "Markdown Link",
+
   "Create/Edit Template": "Create/Edit Template Page",
   "Create/Edit Template": "Create/Edit Template Page",
+
   "Unportalize": "Unportalize",
   "Unportalize": "Unportalize",
+
   "Go to this version": "View this version",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "View diff": "View diff",
   "No diff": "No diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
+
   "User ID": "User ID",
   "User ID": "User ID",
   "Home": "Home",
   "Home": "Home",
   "User Settings": "User Settings",
   "User Settings": "User Settings",
@@ -80,15 +88,19 @@
   "Show": "Show",
   "Show": "Show",
   "Hide": "Hide",
   "Hide": "Hide",
   "Disclose E-mail": "Disclose E-mail",
   "Disclose E-mail": "Disclose E-mail",
+
   "page exists": "this page already exists",
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
   "Error occurred": "Error occurred",
+
   "Create today's": "Create today's ...",
   "Create today's": "Create today's ...",
   "Memo": "memo",
   "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New Page",
   "New Page": "New Page",
   "Create under": "Create page under below:",
   "Create under": "Create page under below:",
+
   "Table of Contents": "Table of Contents",
   "Table of Contents": "Table of Contents",
+
   "Management Wiki Home": "Management Wiki Home",
   "Management Wiki Home": "Management Wiki Home",
   "App Settings": "App Settings",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
   "Site URL settings": "Site URL settings",
@@ -100,7 +112,7 @@
   "UserGroup Management": "UserGroup Management",
   "UserGroup Management": "UserGroup Management",
   "Full Text Search Management": "Full Text Search Management",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Import Data": "Import Data",
-  "Export Archive Data": "Export Archive Data",
+  "Export Data": "Export Data",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
   "Basic authentication": "Basic authentication",
   "Register limitation": "Register limitation",
   "Register limitation": "Register limitation",
@@ -116,24 +128,30 @@
   "Add tags for this page": "Add tags for this page",
   "Add tags for this page": "Add tags for this page",
   "Edit tags for this page": "Edit tags for this page",
   "Edit tags for this page": "Edit tags for this page",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
+
   "Show latest": "Show latest",
   "Show latest": "Show latest",
   "Load latest": "Load latest",
   "Load latest": "Load latest",
   "edited this page": "edited this page.",
   "edited this page": "edited this page.",
+
   "List Drafts": "Drafts",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Sign out": "Logout",
+
   "form_validation": {
   "form_validation": {
     "required": "<code>%s</code> is required"
     "required": "<code>%s</code> is required"
   },
   },
+
   "installer": {
   "installer": {
     "setup": "Setup",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "unavaliable_user_id": "This 'User ID' is unavailable."
     "unavaliable_user_id": "This 'User ID' is unavailable."
   },
   },
+
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
   },
   },
+
   "page_register": {
   "page_register": {
     "notice": {
     "notice": {
       "restricted": "Admin approval required.",
       "restricted": "Admin approval required.",
@@ -145,6 +163,7 @@
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
     }
     }
   },
   },
+
   "page_me": {
   "page_me": {
     "form_help": {
     "form_help": {
       "profile_image1": "Image upload settings not completed.",
       "profile_image1": "Image upload settings not completed.",
@@ -157,8 +176,10 @@
       "update_token1": "You can update to generate a new API Token.",
       "update_token1": "You can update to generate a new API Token.",
       "update_token2": "You will need to update the API Token in any existing processes."
       "update_token2": "You will need to update the API Token in any existing processes."
     },
     },
-    "form_help": {}
+    "form_help": {
+    }
   },
   },
+
   "Password": "Password",
   "Password": "Password",
   "Password Settings": "Password Settings",
   "Password Settings": "Password Settings",
   "Set new Password": "Set new Password",
   "Set new Password": "Set new Password",
@@ -167,11 +188,14 @@
   "New password": "New password",
   "New password": "New password",
   "Re-enter new password": "Re-enter new password",
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
   "Password is not set": "Password is not set",
+
   "security_settings": "Security Settings",
   "security_settings": "Security Settings",
+
   "API Settings": "API Settings",
   "API Settings": "API Settings",
   "API Token Settings": "API Token Settings",
   "API Token Settings": "API Token Settings",
   "Current API Token": "Current API Token",
   "Current API Token": "Current API Token",
   "Update API Token": "Update API Token",
   "Update API Token": "Update API Token",
+
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "This tree": "This tree"
       "This tree": "This tree"
@@ -180,6 +204,7 @@
       "This tree": "Only children of this tree"
       "This tree": "Only children of this tree"
     }
     }
   },
   },
+
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
     "Page path": "Page path",
@@ -187,6 +212,7 @@
     "Page path and parmanent link": "Page path and parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"
     "Markdown link": "Markdown link"
   },
   },
+
   "search_help": {
   "search_help": {
     "title": "Searching Help",
     "title": "Searching Help",
     "and": {
     "and": {
@@ -216,6 +242,7 @@
   "search": {
   "search": {
     "search page bodies": "Hit [Enter] key to full-text search"
     "search page bodies": "Hit [Enter] key to full-text search"
   },
   },
+
   "page_page": {
   "page_page": {
     "notice": {
     "notice": {
       "version": "This is not the current version.",
       "version": "This is not the current version.",
@@ -223,11 +250,10 @@
       "redirected": "You are redirected from <code>%s</code>",
       "redirected": "You are redirected from <code>%s</code>",
       "duplicated": "This page was duplicated from <code>%s</code>",
       "duplicated": "This page was duplicated from <code>%s</code>",
       "unlinked": "Redirect pages to this page have been deleted.",
       "unlinked": "Redirect pages to this page have been deleted.",
-      "restricted": "Access to this page is restricted",
-      "stale": "More than {{count}} year has passed since last update.",
-      "stale_plural": "More than {{count}} years has passed since last update."
+      "restricted": "Access to this page is restricted"
     }
     }
   },
   },
+
   "page_edit": {
   "page_edit": {
     "Show active line": "Show active line",
     "Show active line": "Show active line",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
@@ -235,12 +261,14 @@
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
     }
     }
   },
   },
+
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "already_exists": "New page is already exists.",
     "already_exists": "New page is already exists.",
     "outdated": "Page is updated someone and now outdated.",
     "outdated": "Page is updated someone and now outdated.",
     "user_not_admin": "Only admin user can delete completely"
     "user_not_admin": "Only admin user can delete completely"
   },
   },
+
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "Move/Rename page",
       "Move/Rename page": "Move/Rename page",
@@ -256,8 +284,10 @@
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
     }
   },
   },
+
   "Put Back": "Put Back",
   "Put Back": "Put Back",
   "Delete Completely": "Delete Completely",
   "Delete Completely": "Delete Completely",
+
   "modal_delete": {
   "modal_delete": {
     "delete_page": "Delete Page",
     "delete_page": "Delete Page",
     "deleting_page": "Deleting Page",
     "deleting_page": "Deleting Page",
@@ -267,6 +297,7 @@
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
+
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
       "Duplicate page": "Duplicate page",
       "Duplicate page": "Duplicate page",
@@ -274,6 +305,7 @@
       "Current page name": "Current page name"
       "Current page name": "Current page name"
     }
     }
   },
   },
+
   "modal_putback": {
   "modal_putback": {
     "label": {
     "label": {
       "Put Back Page": "Put Back Page",
       "Put Back Page": "Put Back Page",
@@ -283,6 +315,7 @@
       "recursively": "Put Back children of under <code>%s</code> recursively"
       "recursively": "Put Back children of under <code>%s</code> recursively"
     }
     }
   },
   },
+
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "Global shortcuts",
       "title": "Global shortcuts",
@@ -305,6 +338,7 @@
       "Post": "Post"
       "Post": "Post"
     }
     }
   },
   },
+
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit Template Page",
       "Create/Edit Template Page": "Create/Edit Template Page",
@@ -323,6 +357,7 @@
       "desc": "Applies to all decendant pages"
       "desc": "Applies to all decendant pages"
     }
     }
   },
   },
+
   "sandbox": {
   "sandbox": {
     "header": "Header",
     "header": "Header",
     "header_x": "Header {{index}}",
     "header_x": "Header {{index}}",
@@ -354,6 +389,7 @@
     "insert_image": "inserts an image",
     "insert_image": "inserts an image",
     "open_sandbox": "Open Sandbox"
     "open_sandbox": "Open Sandbox"
   },
   },
+
   "admin_top": {
   "admin_top": {
     "Management Wiki": "Management Wiki",
     "Management Wiki": "Management Wiki",
     "System Information": "System Information",
     "System Information": "System Information",
@@ -364,6 +400,7 @@
     "Specified version": "Specified version",
     "Specified version": "Specified version",
     "Installed version": "Installed version"
     "Installed version": "Installed version"
   },
   },
+
   "app_setting": {
   "app_setting": {
     "Site Name": "Site name",
     "Site Name": "Site name",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
@@ -373,7 +410,7 @@
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "Confidential name": "Confidential name",
     "Confidential name": "Confidential name",
     "Default Language for new users": "Default Language for new users",
     "Default Language for new users": "Default Language for new users",
-    "ex) internal use only":"ex): internal use only",
+    "ex): internal use only":"ex): internal use only",
     "File Uploading": "File Uploading",
     "File Uploading": "File Uploading",
     "enable_files_except_image": "Enable file upload other than image files.",
     "enable_files_except_image": "Enable file upload other than image files.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
@@ -383,7 +420,7 @@
     "SMTP_but_AWS": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "SMTP_but_AWS": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
     "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
     "From e-mail address": "From e-mail address",
     "From e-mail address": "From e-mail address",
-    "SMTP settings": "SMTP settings",
+    "SMTP settings": "SMTP settings"  ,
     "Host": "Host",
     "Host": "Host",
     "Port": "Port",
     "Port": "Port",
     "User": "User",
     "User": "User",
@@ -400,23 +437,21 @@
     "Load plugins": "Load plugins",
     "Load plugins": "Load plugins",
     "Enable": "Enable",
     "Enable": "Enable",
     "Disable": "Disable",
     "Disable": "Disable",
-    "Use env var if empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used.",
-    "updated_app_setting": "Succeeded to update app setting",
-    "updated_site_url": "Succeeded to update site URL",
-    "updated_plugin_setting": "Succeeded to update plugin setting"
+    "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>%s</code> is used."
   },
   },
+
   "security_setting": {
   "security_setting": {
-    "Security settings": "Security settings",
+		"Security settings": "Security settings",
     "Guest Users Access": "Guest Users Access",
     "Guest Users Access": "Guest Users Access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
     "Register limitation": "Register limitation",
     "Register limitation desc": "Restricts ways to register new user.",
     "Register limitation desc": "Restricts ways to register new user.",
-    "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-    "users_without_account": "Users without account is not accessible",
+		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
+		"users_without_account": "Users without account is not accessible",
     "example": "Example",
     "example": "Example",
     "restrict_emails": "You can restrict registerable e-mail address.",
     "restrict_emails": "You can restrict registerable e-mail address.",
-    "for_instance": " For instance, if you use growi within a company, you can write ",
-    "only_those": " Only those whose e-mail address including the company address can register.",
+		"for_instance": " For instance, if you use growi within a company, you can write ",
+		"only_those": " Only those whose e-mail address including the company address can register.",
     "insert_single": "Please insert single e-mail address per line.",
     "insert_single": "Please insert single e-mail address per line.",
     "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
     "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
@@ -427,10 +462,11 @@
     "admin_only": "Admin Only",
     "admin_only": "Admin Only",
     "admin_and_author": "Admin and Author",
     "admin_and_author": "Admin and Author",
     "anyone": "Anyone",
     "anyone": "Anyone",
-    "Authentication mechanism settings": "Authentication Mechanism Settings",
+
+		"Authentication mechanism settings": "Authentication Mechanism Settings",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
-    "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
-    "xss_prevent_setting_link": "Go to Markdown settings",
+    "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
+    "xss_prevent_setting_link":"Go to Markdown settings",
     "callback_URL": "Callback URL",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
     "issuerHost": "Issuer Host",
@@ -559,14 +595,15 @@
       "security:passport-saml:attrMapFirstName": "First Name",
       "security:passport-saml:attrMapFirstName": "First Name",
       "security:passport-saml:attrMapLastName": "Last Name"
       "security:passport-saml:attrMapLastName": "Last Name"
     }
     }
-  },
+	},
+
   "markdown_setting": {
   "markdown_setting": {
     "line_break_setting": "Line Break Setting",
     "line_break_setting": "Line Break Setting",
     "line_break_setting_desc": "You can change line break settings.",
     "line_break_setting_desc": "You can change line break settings.",
     "Enable Line Break": "Enable Line Break",
     "Enable Line Break": "Enable Line Break",
-    "Enable Line Break desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
+    "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
     "Enable Line Break for comment": "Enable Line Break in comment",
     "Enable Line Break for comment": "Enable Line Break in comment",
-    "Enable Line Break for comment desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML",
+    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
     "presentation_setting": "Presentation Setting",
     "presentation_setting": "Presentation Setting",
     "presentation_setting_desc": "You can change presentation settings.",
     "presentation_setting_desc": "You can change presentation settings.",
     "Page break setting": "Page break Setting",
     "Page break setting": "Page break Setting",
@@ -585,13 +622,11 @@
     "Ignore all tags desc": "Stripe all HTML tags and attributes",
     "Ignore all tags desc": "Stripe all HTML tags and attributes",
     "Recommended setting": "Recommended Setting",
     "Recommended setting": "Recommended Setting",
     "Custom Whitelist": "Custom Whitelist",
     "Custom Whitelist": "Custom Whitelist",
-    "Tag names": "Tag names",
-    "Tag attributes": "Tag attributes",
-    "import_recommended": "Import recommended {{target}}",
-    "updated_lineBreak": "Succeeded to update line braek setting",
-    "updated_presentation": "Succeeded to update presentation setting",
-    "updated_xss": "Succeeded to update XSS setting"
+    "Tag names":"Tag names",
+    "Tag attributes":"Tag attributes",
+    "import_recommended": "Import recommended %s"
   },
   },
+
   "notification_setting": {
   "notification_setting": {
     "notification_list": "List of Notification Settings",
     "notification_list": "List of Notification Settings",
     "add_notification": "Add New",
     "add_notification": "Add New",
@@ -611,8 +646,8 @@
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
     }
     }
   },
   },
+
   "customize_page": {
   "customize_page": {
-    "recommended": "Recommended",
     "Behavior": "Behavior",
     "Behavior": "Behavior",
     "Layout": "Layout",
     "Layout": "Layout",
     "Function": "Function",
     "Function": "Function",
@@ -630,7 +665,7 @@
     "custom_title": "Custom Title",
     "custom_title": "Custom Title",
     "custom_title_detail": "You can customize <code>%s</code> tag.<br><code>%s</code> will be automatically replaced with the app name, and <code>%s</code> will be replaced with the page name/path.",
     "custom_title_detail": "You can customize <code>%s</code> tag.<br><code>%s</code> will be automatically replaced with the app name, and <code>%s</code> will be replaced with the page name/path.",
     "custom_header": "Custom HTML Header",
     "custom_header": "Custom HTML Header",
-    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
+    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>%s</code> but above other <code>%s</code> tags.<br>Relaod page to see changes.",
     "Custom CSS": "Custom CSS",
     "Custom CSS": "Custom CSS",
     "write_CSS": "You can write CSS that is applied to whole system.",
     "write_CSS": "You can write CSS that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "reflect_change": "You need to reload the page to reflect the change.",
@@ -640,45 +675,11 @@
     "attach_title_header": "Add h1 section when create new page automatically",
     "attach_title_header": "Add h1 section when create new page automatically",
     "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
     "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
     "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
     "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
-    "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
-    "stale_notification": "Display Notification on Stale Pages",
-    "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
-    "update_layout_success": "Succeeded to update layout",
-    "update_behavior_success": "Succeeded to update behavior",
-    "update_function_success": "Succeeded to update function",
-    "update_highlight_success": "Succeeded to update code highlight",
-    "update_customTitle_success": "Succeeded to update customize title",
-    "update_customHeader_success": "Succeeded to update customize html header",
-    "update_customCss_success": "Succeeded to update customize css",
-    "update_script_success": "Succeeded to update custom script",
-    "layout_description": {
-      "growi_title": "Simple and Clear",
-      "growi_text1": "Full screen layout and thin margins/paddings",
-      "growi_text2": "Show and post comments at the bottom of the page",
-      "growi_text3": "Affix Table-of-contents",
-      "kibela_title": "Easy Viewing Structure",
-      "kibela_text1": "Center aligned contents",
-      "kibela_text2": "Show and post comments at the bottom of the page",
-      "kibela_text3": "Affix Table-of-contents",
-      "crowi_title": "Separated Functions",
-      "crowi_text1": "Collapsible Sidebar",
-      "crowi_text2": "Show and post comments in Sidebar",
-      "crowi_text3": "Collapsible Table-of-contents"
-    },
-    "behavior_description": {
-      "growi_text1": "Both of <code>/page</code> and <code>/page/</code> shows the same page。",
-      "growi_text2": "<code>/nonexistent_page</code> shows editing form",
-      "growi_text3": "All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
-      "crowi_text1": "<code>/page</code> shows the page",
-      "crowi_text2": "<code>/page/</code> shows the list of sub pages",
-      "crowi_text3": "If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
-      "crowi_text4": "<code>/nonexistent_page</code> shows editing form<",
-      "crowi_text5": "<code>/nonexistent_page/</code> the list of sub pages"
-    }
+    "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page"
   },
   },
+
   "user_management": {
   "user_management": {
     "target_user": "Target User",
     "target_user": "Target User",
-    "new_password": "New Password",
     "invite_users": "Invite New Users",
     "invite_users": "Invite New Users",
     "emails": "Emails",
     "emails": "Emails",
     "invite_thru_email": "Send Invitation Email",
     "invite_thru_email": "Send Invitation Email",
@@ -698,42 +699,28 @@
     "unset": "No",
     "unset": "No",
     "temporary_password": "The created user has a temporary password",
     "temporary_password": "The created user has a temporary password",
     "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
     "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
-    "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
     "send_new_password": "Please send the new password to the user.",
     "send_new_password": "Please send the new password to the user.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",
-    "related_username": "Related user's ",
+    "related_username": "Related user's <code>%s</code>",
     "accept": "Accept",
     "accept": "Accept",
-    "deactivate_account": "Deactivate Account",
-    "your_own": "You cannot deactivate your own account",
-    "administrator_menu": "Administrator Menu",
-    "cannot_remove": "You cannot remove yourself from administrator",
+    "deactivate_account":"Deactivate Account",
+    "your_own":"You cannot deactivate your own account",
+    "administrator_menu":"Administrator Menu",
+    "cannot_remove":"You cannot remove yourself from administrator",
     "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
     "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
-    "current_users": "Current users:",
-    "valid_email": "Valid email address is required",
-    "existing_email": "The following emails already exist",
-    "give_user_admin": "Succeeded to give {{username}} admin",
-    "remove_user_admin": "Succeeded to remove {{username}} admin ",
-    "activate_user_success": "Succeeded to activating {{username}}",
-    "deactivate_user_success": "Succeeded to deactivate {{username}}",
-    "remove_user_success": "Succeeded to removing {{username}} ",
-    "remove_external_user_success": "Succeeded to remove {{accountId}} "
+    "current_users": "Current users:"
   },
   },
+
   "user_group_management": {
   "user_group_management": {
-    "search_option": "Search Option",
-    "enable_option": "Enable {{option}}",
-    "forward_match": "forword match",
-    "partial_match": "partial match",
-    "backward_match": "backward match",
     "group_list": "Group List",
     "group_list": "Group List",
     "back_to_list": "Go Back to Group List",
     "back_to_list": "Go Back to Group List",
-    "basic_info": "Basic Info",
-    "user_list": "User List",
     "create_group": "Create New Group",
     "create_group": "Create New Group",
     "group_example": "e.g. : Group1",
     "group_example": "e.g. : Group1",
     "created_group": "Group was created",
     "created_group": "Group was created",
     "add_user": "Add a User to the Created Group",
     "add_user": "Add a User to the Created Group",
-    "deny_create_group": "You can't create a new group with the current settings",
+    "deny_create_group": "You can't create a new group.",
+    "is_loading_data": "Loading data...",
     "choose_action": "Choose an action for private pages",
     "choose_action": "Choose an action for private pages",
     "delete_group": "Delete Group",
     "delete_group": "Delete Group",
     "group_name": "Group Name",
     "group_name": "Group Name",
@@ -744,51 +731,23 @@
     "select_group": "Select a group",
     "select_group": "Select a group",
     "no_groups": "No groups to select",
     "no_groups": "No groups to select",
     "no_pages": "There are no pages the group has view permission",
     "no_pages": "There are no pages the group has view permission",
+    "how_to_add1": "Enter a username to add",
+    "how_to_add2": "Select a user from user list",
     "remove_from_group": "Remove this user"
     "remove_from_group": "Remove this user"
   },
   },
+
   "importer_management": {
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",
-    "import_from": "Import from {{from}}",
-    "import_growi_archive": "Import GROWI Archive",
+    "import_from": "Import from %s",
+    "import_form_growi": "Import from GROWI",
     "growi_settings": {
     "growi_settings": {
       "overwrite_documents": "Imported documents will overwrite existing documents",
       "overwrite_documents": "Imported documents will overwrite existing documents",
-      "growi_archive_file": "GROWI Archive File",
+      "zip_file": "Zip File",
       "uploaded_data": "Uploaded Data",
       "uploaded_data": "Uploaded Data",
       "extracted_file": "Extracted File",
       "extracted_file": "Extracted File",
       "collection": "Collection",
       "collection": "Collection",
       "upload": "Upload",
       "upload": "Upload",
-      "discard": "Discard Uploaded Data",
-      "errors": {
-        "at_least_one": "Select one or more collections.",
-        "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
-        "depends": "'{{target}}' must be selected when '{{condition}}' is selected."
-      },
-      "configuration": {
-        "pages": {
-          "overwrite_author": {
-            "label": "Overwrite page's author with the current user",
-            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-          },
-          "set_public_to_page": {
-            "label": "Set 'Public' to the pages that is '{{from}}'",
-            "desc": "Make sure that this configuration makes all <b>'{{from}}'</b> pages readable from <span class=\"text-danger\">ANY users</span>."
-          },
-          "initialize_meta_datas": {
-            "label": "Initialize page's like, read users and comment count",
-            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "Initialize HackMD related data",
-            "desc": "Recommended to check this unless there is important drafts on HackMD."
-          }
-        },
-        "revisions": {
-          "overwrite_author": {
-            "label": "Overwrite revision's author with the current user",
-            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-          }
-        }
-      }
+      "discard": "Discard Uploaded Data"
     },
     },
     "esa_settings": {
     "esa_settings": {
       "team_name": "Team name",
       "team_name": "Team name",
@@ -804,21 +763,14 @@
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
   },
   },
-  "full_text_search_management": {
-    "elasticsearch_management": "Elasticsearch Management",
-    "build_button": "Rebuild Index",
-    "rebuild_description_1": "Force rebuild index.",
-    "rebuild_description_2": "Click 'Build Now' to delete and create mapping file and add all pages.",
-    "rebuild_description_3": "This may take a while."
-  },
+
   "export_management": {
   "export_management": {
-    "exporting_collection_list": "Exporting Collection List",
-    "exported_data_list": "Exported Archive Data List",
+    "beta_warning": "This function is Beta.",
+    "exported_data_list": "Exported Data List",
     "export_collections": "Export Collections",
     "export_collections": "Export Collections",
     "check_all": "Check All",
     "check_all": "Check All",
     "uncheck_all": "Uncheck All",
     "uncheck_all": "Uncheck All",
-    "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
-    "create_new_archive_data": "Create New Archive Data",
+    "create_new_exported_data": "Create New Exported Data",
     "export": "Export",
     "export": "Export",
     "cancel": "Cancel",
     "cancel": "Cancel",
     "file": "File",
     "file": "File",

+ 82 - 131
resource/locales/ja/translation.json

@@ -21,7 +21,6 @@
   "New": "作成",
   "New": "作成",
   "Shortcuts": "ショートカット",
   "Shortcuts": "ショートカット",
   "eg": "例:",
   "eg": "例:",
-  "add": "追加",
   "Undo": "元に戻す",
   "Undo": "元に戻す",
   "Article": "記事",
   "Article": "記事",
   "Page": "ページ",
   "Page": "ページ",
@@ -30,9 +29,11 @@
   "User": "ユーザー",
   "User": "ユーザー",
   "status": "ステータス",
   "status": "ステータス",
   "account_id": "アカウントID",
   "account_id": "アカウントID",
+
   "Update": "更新",
   "Update": "更新",
   "Update Page": "ページを更新",
   "Update Page": "ページを更新",
   "Warning": "注意",
   "Warning": "注意",
+
   "Sign in": "ログイン",
   "Sign in": "ログイン",
   "Sign up is here": "新規登録はこちら",
   "Sign up is here": "新規登録はこちら",
   "Sign in is here": "ログインはこちら",
   "Sign in is here": "ログインはこちら",
@@ -42,23 +43,29 @@
   "Sign up with this Google Account": "この Google アカウントで登録します",
   "Sign up with this Google Account": "この Google アカウントで登録します",
   "Example": "例",
   "Example": "例",
   "Taro Yamada": "山田 太郎",
   "Taro Yamada": "山田 太郎",
+
   "List View": "リスト表示",
   "List View": "リスト表示",
   "Timeline View": "タイムライン表示",
   "Timeline View": "タイムライン表示",
   "History": "更新履歴",
   "History": "更新履歴",
   "Presentation Mode": "プレゼンテーション",
   "Presentation Mode": "プレゼンテーション",
-  "username": "ユーザー名",
+
   "Created": "作成日",
   "Created": "作成日",
   "Last updated": "最終更新",
   "Last updated": "最終更新",
   "Last_Login": "最終ログイン",
   "Last_Login": "最終ログイン",
+
   "Share": "共有",
   "Share": "共有",
   "Share Link": "共有用リンク",
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
   "Markdown Link": "Markdown形式のリンク",
+
   "Create/Edit Template": "テンプレートページの作成/編集",
   "Create/Edit Template": "テンプレートページの作成/編集",
+
   "Unportalize": "ポータル解除",
   "Unportalize": "ポータル解除",
+
   "Go to this version": "このバージョンを見る",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
+
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
   "Home": "ホーム",
   "Home": "ホーム",
   "User Settings": "ユーザー設定",
   "User Settings": "ユーザー設定",
@@ -80,15 +87,19 @@
   "Show": "公開",
   "Show": "公開",
   "Hide": "非公開",
   "Hide": "非公開",
   "Disclose E-mail": "メールアドレスの公開",
   "Disclose E-mail": "メールアドレスの公開",
+
   "page exists": "このページはすでに存在しています",
   "page exists": "このページはすでに存在しています",
-  "Error occurred": "エラーが発生しました",
+  "Error occurred":"エラーが発生しました",
+
   "Create today's": "今日の◯◯を作成",
   "Create today's": "今日の◯◯を作成",
   "Memo": "メモ",
   "Memo": "メモ",
   "Input page name": "ページ名を入力",
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
   "Create under": "ページを以下に作成",
+
   "Table of Contents": "目次",
   "Table of Contents": "目次",
+
   "Management Wiki Home": "Wiki管理トップ",
   "Management Wiki Home": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "App Settings": "アプリ設定",
   "Site URL settings": "サイトURL設定",
   "Site URL settings": "サイトURL設定",
@@ -100,7 +111,7 @@
   "UserGroup Management": "グループ管理",
   "UserGroup Management": "グループ管理",
   "Full Text Search Management": "全文検索管理",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
   "Import Data": "データインポート",
-  "Export Archive Data": "データアーカイブ",
+  "Export Data": "データエクスポート",
   "Basic Settings": "基本設定",
   "Basic Settings": "基本設定",
   "Register limitation": "登録の制限",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
@@ -115,28 +126,34 @@
   "Add tags for this page": "タグを付ける",
   "Add tags for this page": "タグを付ける",
   "Edit tags for this page": "タグを編集する",
   "Edit tags for this page": "タグを編集する",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
+
   "Show latest": "最新のページを表示",
   "Show latest": "最新のページを表示",
   "Load latest": "最新版を読み込む",
   "Load latest": "最新版を読み込む",
   "edited this page": "さんがこのページを編集しました。",
   "edited this page": "さんがこのページを編集しました。",
+
   "List Drafts": "下書き一覧",
   "List Drafts": "下書き一覧",
   "Deleted Pages": "削除済みページ",
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Sign out": "ログアウト",
+
   "form_validation": {
   "form_validation": {
     "required": "<code>%s</code> に値を入力してください"
     "required": "<code>%s</code> に値を入力してください"
   },
   },
+
   "installer": {
   "installer": {
     "setup": "セットアップ",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "unavaliable_user_id": "このユーザーIDは利用できません。"
     "unavaliable_user_id": "このユーザーIDは利用できません。"
   },
   },
+
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
   },
   },
+
   "page_register": {
   "page_register": {
     "notice": {
     "notice": {
-      "restricted": "この Wiki への新規登録は制限されています。",
-      "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。"
+       "restricted": "この Wiki への新規登録は制限されています。",
+       "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。"
     },
     },
     "form_help": {
     "form_help": {
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
@@ -144,6 +161,7 @@
       "user_id": "ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。"
       "user_id": "ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。"
     }
     }
   },
   },
+
   "page_me": {
   "page_me": {
     "form_help": {
     "form_help": {
       "profile_image1": "画像をアップロードをするための設定がされていません。",
       "profile_image1": "画像をアップロードをするための設定がされていません。",
@@ -156,8 +174,10 @@
       "update_token1": "API Token を更新すると、自動的に新しい Token が生成されます。",
       "update_token1": "API Token を更新すると、自動的に新しい Token が生成されます。",
       "update_token2": "現在の Token を利用している処理は動かなくなります。"
       "update_token2": "現在の Token を利用している処理は動かなくなります。"
     },
     },
-    "form_help": {}
+    "form_help": {
+    }
   },
   },
+
   "Password": "パスワード",
   "Password": "パスワード",
   "Password Settings": "パスワード設定",
   "Password Settings": "パスワード設定",
   "Set new Password": "パスワードを新規に設定",
   "Set new Password": "パスワードを新規に設定",
@@ -166,11 +186,14 @@
   "New password": "新しいパスワード",
   "New password": "新しいパスワード",
   "Re-enter new password": "(確認用)",
   "Re-enter new password": "(確認用)",
   "Password is not set": "パスワードが設定されていません",
   "Password is not set": "パスワードが設定されていません",
+
   "security_settings": "セキュリティ設定",
   "security_settings": "セキュリティ設定",
+
   "API Settings": "API設定",
   "API Settings": "API設定",
   "API Token Settings": "API Token設定",
   "API Token Settings": "API Token設定",
   "Current API Token": "現在のAPI Token",
   "Current API Token": "現在のAPI Token",
   "Update API Token": "API Tokenを更新",
   "Update API Token": "API Tokenを更新",
+
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "This tree": "この階層"
       "This tree": "この階層"
@@ -179,6 +202,7 @@
       "This tree": "この階層下の子ページのみ"
       "This tree": "この階層下の子ページのみ"
     }
     }
   },
   },
+
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
     "Page path": "ページ名",
@@ -186,6 +210,7 @@
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Markdown link": "マークダウン形式のリンク"
     "Markdown link": "マークダウン形式のリンク"
   },
   },
+
   "search_help": {
   "search_help": {
     "title": "検索のヘルプ",
     "title": "検索のヘルプ",
     "and": {
     "and": {
@@ -215,6 +240,7 @@
   "search": {
   "search": {
     "search page bodies": "[Enter] キー押下で全文検索"
     "search page bodies": "[Enter] キー押下で全文検索"
   },
   },
+
   "page_page": {
   "page_page": {
     "notice": {
     "notice": {
       "version": "これは現在の版ではありません。",
       "version": "これは現在の版ではありません。",
@@ -222,10 +248,10 @@
       "redirected": "リダイレクト元 >> <code>%s</code>",
       "redirected": "リダイレクト元 >> <code>%s</code>",
       "duplicated": "このページは <code>%s</code> から複製されました。",
       "duplicated": "このページは <code>%s</code> から複製されました。",
       "unlinked": "このページへのリダイレクトは削除されました。",
       "unlinked": "このページへのリダイレクトは削除されました。",
-      "restricted": "このページの閲覧は制限されています",
-      "stale": "このページは最終更新日から{{count}}年以上が経過しています。"
+      "restricted": "このページの閲覧は制限されています"
     }
     }
   },
   },
+
   "page_edit": {
   "page_edit": {
     "Show active line": "アクティブ行をハイライト",
     "Show active line": "アクティブ行をハイライト",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
@@ -233,12 +259,14 @@
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     }
     }
   },
   },
+
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "already_exists": "新しいページが既に存在しています。",
     "already_exists": "新しいページが既に存在しています。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
   },
   },
+
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "ページを移動/名前変更する",
       "Move/Rename page": "ページを移動/名前変更する",
@@ -254,8 +282,10 @@
       "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
       "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
     }
     }
   },
   },
+
   "Put Back": "元に戻す",
   "Put Back": "元に戻す",
   "Delete Completely": "完全削除",
   "Delete Completely": "完全削除",
+
   "modal_delete": {
   "modal_delete": {
     "delete_page": "ページを削除する",
     "delete_page": "ページを削除する",
     "deleting_page": "ページパス",
     "deleting_page": "ページパス",
@@ -265,6 +295,7 @@
     "recursively": "<code>%s</code> 配下のページも削除します",
     "recursively": "<code>%s</code> 配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   },
+
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
       "Duplicate page": "ページを複製する",
       "Duplicate page": "ページを複製する",
@@ -272,6 +303,7 @@
       "Current page name": "現在のページ名"
       "Current page name": "現在のページ名"
     }
     }
   },
   },
+
   "modal_putback": {
   "modal_putback": {
     "label": {
     "label": {
       "Put Back Page": "ページを元に戻す",
       "Put Back Page": "ページを元に戻す",
@@ -281,6 +313,7 @@
       "recursively": "<code>%s</code> 配下のページも元に戻します"
       "recursively": "<code>%s</code> 配下のページも元に戻します"
     }
     }
   },
   },
+
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "グローバルショートカット",
       "title": "グローバルショートカット",
@@ -303,6 +336,7 @@
       "Post": "投稿"
       "Post": "投稿"
     }
     }
   },
   },
+
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
       "Create/Edit Template Page": "テンプレートページの作成/編集",
@@ -321,6 +355,7 @@
       "desc": "テンプレートページが存在する下位層のすべてのページに適用されます"
       "desc": "テンプレートページが存在する下位層のすべてのページに適用されます"
     }
     }
   },
   },
+
   "sandbox": {
   "sandbox": {
     "header": "見出し",
     "header": "見出し",
     "header_x": "見出し {{index}}",
     "header_x": "見出し {{index}}",
@@ -352,6 +387,7 @@
     "insert_image": "で画像を挿入できます",
     "insert_image": "で画像を挿入できます",
     "open_sandbox": "Sandbox を開く"
     "open_sandbox": "Sandbox を開く"
   },
   },
+
   "admin_top": {
   "admin_top": {
     "Management Wiki": "Wiki管理",
     "Management Wiki": "Wiki管理",
     "System Information": "システム情報",
     "System Information": "システム情報",
@@ -362,6 +398,7 @@
     "Specified version": "指定バージョン",
     "Specified version": "指定バージョン",
     "Installed version": "インストールされているバージョン"
     "Installed version": "インストールされているバージョン"
   },
   },
+
   "app_setting": {
   "app_setting": {
     "Site Name": "サイト名",
     "Site Name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
@@ -371,7 +408,7 @@
     "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
     "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
     "Confidential name": "コンフィデンシャル表示",
     "Confidential name": "コンフィデンシャル表示",
     "Default Language for new users": "新規ユーザーのデフォルト設定言語",
     "Default Language for new users": "新規ユーザーのデフォルト設定言語",
-    "ex) internal use only": "例: 社外秘",
+    "ex): internal use only": "例: 社外秘",
     "File Uploading": "ファイルアップロード",
     "File Uploading": "ファイルアップロード",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
@@ -381,7 +418,7 @@
     "SMTP_but_AWS": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
     "SMTP_but_AWS": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
     "neihter_of": "どちらの設定もない場合、メールは送信されません。",
     "neihter_of": "どちらの設定もない場合、メールは送信されません。",
     "From e-mail address": "Fromアドレス",
     "From e-mail address": "Fromアドレス",
-    "SMTP settings": "SMTP設定",
+    "SMTP settings": "SMTP設定"   ,
     "Host": "ホスト",
     "Host": "ホスト",
     "Port": "ポート",
     "Port": "ポート",
     "User": "ユーザー",
     "User": "ユーザー",
@@ -398,11 +435,9 @@
     "Load plugins": "プラグインを読み込む",
     "Load plugins": "プラグインを読み込む",
     "Enable": "有効",
     "Enable": "有効",
     "Disable": "無効",
     "Disable": "無効",
-    "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
-    "updated_app_setting": "アプリ設定を更新しました",
-    "updated_site_url": "サイトURLを更新しました",
-    "updated_plugin_setting": "プラグイン設定を更新しました"
-  },
+    "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>%s</code> の値を利用します"
+   },
+
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
     "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
@@ -412,9 +447,9 @@
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
-    "for_instance": "例えば、",
-    "only_those": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
-    "insert_single": "1行に1メールアドレス入力してください。",
+    "for_instance":"例えば、",
+    "only_those":"と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
+    "insert_single":"1行に1メールアドレス入力してください。",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
@@ -424,10 +459,11 @@
     "admin_only": "管理者のみ可能",
     "admin_only": "管理者のみ可能",
     "admin_and_author": "管理者とページ作者が可能",
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
     "anyone": "誰でも可能",
-    "Authentication mechanism settings": "認証機構設定",
+
+    "Authentication mechanism settings":"認証機構設定",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
-    "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
-    "xss_prevent_setting_link": "マークダウン設定ページに移動",
+    "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
+    "xss_prevent_setting_link":"マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "callback_URL": "コールバックURL",
     "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
     "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
     "clientID": "クライアントID",
     "clientID": "クライアントID",
@@ -543,6 +579,7 @@
       "security:passport-saml:attrMapLastName": "名"
       "security:passport-saml:attrMapLastName": "名"
     }
     }
   },
   },
+
   "markdown_setting": {
   "markdown_setting": {
     "line_break_setting": "Line Break設定",
     "line_break_setting": "Line Break設定",
     "line_break_setting_desc": "Line Breakの設定を変更できます。",
     "line_break_setting_desc": "Line Breakの設定を変更できます。",
@@ -568,13 +605,11 @@
     "Ignore all tags desc": "すべてのHTMLタグと属性を使用不可にします",
     "Ignore all tags desc": "すべてのHTMLタグと属性を使用不可にします",
     "Recommended setting": "おすすめ設定",
     "Recommended setting": "おすすめ設定",
     "Custom Whitelist": "カスタムホワイトリスト",
     "Custom Whitelist": "カスタムホワイトリスト",
-    "Tag names": "タグ名",
-    "Tag attributes": "タグ属性",
-    "import_recommended": "{{target}} のおすすめをインポート",
-    "updated_lineBreak": "改行設定を更新しました",
-    "updated_presentation": "プレゼンテーション設定を更新しました",
-    "updated_xss": "XSS設定を更新しました"
+    "Tag names": "タグ名のホワイトリスト",
+    "Tag attributes": "タグ属性のホワイトリスト",
+    "import_recommended": "おすすめをインポート"
   },
   },
+
   "notification_setting": {
   "notification_setting": {
     "notification_list": "通知設定の一覧",
     "notification_list": "通知設定の一覧",
     "add_notification": "通知設定の追加",
     "add_notification": "通知設定の追加",
@@ -594,9 +629,9 @@
       "ifttt_link": "IFTTT でメールトリガの新しいアプレットを作る"
       "ifttt_link": "IFTTT でメールトリガの新しいアプレットを作る"
     }
     }
   },
   },
+
   "customize_page": {
   "customize_page": {
-    "recommended": "おすすめ",
-    "Behavior": "動作",
+    "Behavior": "挙動",
     "Layout": "レイアウト",
     "Layout": "レイアウト",
     "Function": "機能",
     "Function": "機能",
     "function_choose": "機能の有効/無効を選択できます。",
     "function_choose": "機能の有効/無効を選択できます。",
@@ -613,7 +648,7 @@
     "custom_title": "カスタム Title",
     "custom_title": "カスタム Title",
     "custom_title_detail": "<code>%s</code>タグのコンテンツをカスタマイズできます。<br><code>%s</code>がサイト名、<code>%s</code>がページ名またはページパスに置換されます。",
     "custom_title_detail": "<code>%s</code>タグのコンテンツをカスタマイズできます。<br><code>%s</code>がサイト名、<code>%s</code>がページ名またはページパスに置換されます。",
     "custom_header": "カスタム HTML Header",
     "custom_header": "カスタム HTML Header",
-    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
+    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>%s</code> タグ内の他の <code>%s</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
     "Custom CSS": "カスタム CSS",
     "Custom CSS": "カスタム CSS",
     "write_CSS": " システム全体に適用されるCSSを記述できます。",
     "write_CSS": " システム全体に適用されるCSSを記述できます。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "reflect_change": "変更の反映はページの更新が必要です。",
@@ -623,45 +658,11 @@
     "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
     "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
     "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
     "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
     "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
     "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
-    "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。",
-    "stale_notification": "更新されていないページに通知を表示",
-    "stale_notification_desc": "最終更新から1年以上が経過しているページに通知を表示します。",
-    "update_layout_success": "レイアウトを更新しました",
-    "update_behavior_success": "動作を更新しました",
-    "update_function_success": "機能を更新しました",
-    "update_highlight_success": "コードハイライトを更新しました",
-    "update_customTitle_success": "カスタムタイトルを更新しました",
-    "update_customHeader_success": "カスタムHTMLヘッダーを更新しました",
-    "update_customCss_success": "カスタムCSSを更新しました",
-    "update_script_success": "カスタムスクリプトを更新しました",
-    "layout_description": {
-      "growi_title": "シンプル・明瞭",
-      "growi_text1": "全画面レイアウトで、余白は少なくなります。",
-      "growi_text2": "コメントはページの下部に表示されます。",
-      "growi_text3": "ページ情報は下部に表示されます。",
-      "kibela_title": "閲覧重視の構造",
-      "kibela_text1": "コンテンツが中心に表示されます。",
-      "kibela_text2": "コメントはページの下部に表示されます。",
-      "kibela_text3": "ページ情報は下部に表示されます。",
-      "crowi_title": "ビュー・コントロールの分離",
-      "crowi_text1": "サイドバーを開くと情報が表示されます。",
-      "crowi_text2": "コメントはサイドバーに表示されます。",
-      "crowi_text3": "ページ情報はサイドバーに表示されます。"
-    },
-    "behavior_description": {
-      "growi_text1": "<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
-      "growi_text2": "<code>/nonexistent_page</code> では編集フォームを表示します",
-      "growi_text3": "<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
-      "crowi_text1": "<code>/page</code> ではページを表示します。",
-      "crowi_text2": "<code>/page/</code> では配下のページを表示します。",
-      "crowi_text3": "<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
-      "crowi_text4": "<code>/nonexistent_page</code> では編集フォームを表示します",
-      "crowi_text5": "<code>/nonexistent_page</code> では配下のページリストを表示します。"
-    }
+    "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。"
   },
   },
+
   "user_management": {
   "user_management": {
     "target_user": "対象ユーザー",
     "target_user": "対象ユーザー",
-    "new_password": "新しいパスワード",
     "invite_users": "新規ユーザーの招待",
     "invite_users": "新規ユーザーの招待",
     "emails": "メールアドレス (複数行入力で複数人招待可能)",
     "emails": "メールアドレス (複数行入力で複数人招待可能)",
     "invite_thru_email": "招待をメールで送信",
     "invite_thru_email": "招待をメールで送信",
@@ -681,37 +682,22 @@
     "unset": "未設定",
     "unset": "未設定",
     "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
     "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
     "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
     "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
-    "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "reset_password": "パスワードの再発行",
     "reset_password": "パスワードの再発行",
-    "related_username": "関連付けられているユーザーの ",
+    "related_username": "関連付けられているユーザーの <code>%s</code>",
     "accept": "承認する",
     "accept": "承認する",
     "deactivate_account": "アカウント停止",
     "deactivate_account": "アカウント停止",
     "your_own": "自分自身のアカウントを停止することはできません",
     "your_own": "自分自身のアカウントを停止することはできません",
     "administrator_menu": "管理者メニュー",
     "administrator_menu": "管理者メニュー",
     "cannot_remove": "自分自身を管理者から外すことはできません",
     "cannot_remove": "自分自身を管理者から外すことはできません",
     "cannot_invite_maximum_users": "ユーザーが上限に達したため招待できません。",
     "cannot_invite_maximum_users": "ユーザーが上限に達したため招待できません。",
-    "current_users": "現在のユーザー数:",
-    "valid_email": "メールアドレスを入力してください。",
-    "existing_email": "以下のEmailはすでに存在しています。",
-    "give_user_admin": "{{username}}を管理者に設定しました",
-    "remove_user_admin": "{{username}}を管理者から外しました",
-    "activate_user_success": "{{username}}を有効化しました",
-    "deactivate_user_success": "{{username}}を無効化しました",
-    "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "current_users": "現在のユーザー数:"
   },
   },
+
   "user_group_management": {
   "user_group_management": {
-    "search_option": "検索オプション",
-    "enable_option": "{{option}}を有効にする",
-    "forward_match": "前方一致",
-    "partial_match": "部分一致",
-    "backward_match": "後方一致",
     "group_list": "グループ一覧",
     "group_list": "グループ一覧",
     "back_to_list": "グループ一覧に戻る",
     "back_to_list": "グループ一覧に戻る",
-    "basic_info": "基本情報",
-    "user_list": "ユーザー一覧",
     "create_group": "新規グループの作成",
     "create_group": "新規グループの作成",
     "group_example": "例: Group1",
     "group_example": "例: Group1",
     "created_group": "グループを作成しました",
     "created_group": "グループを作成しました",
@@ -728,51 +714,23 @@
     "select_group": "グループを選択してください",
     "select_group": "グループを選択してください",
     "no_groups": "グループがありません",
     "no_groups": "グループがありません",
     "no_pages": "グループが閲覧権限を保有するページはありません",
     "no_pages": "グループが閲覧権限を保有するページはありません",
+    "how_to_add1": "ユーザー名を入力して追加",
+    "how_to_add2": "ユーザーを下のリストから選択",
     "remove_from_group": "グループから外す"
     "remove_from_group": "グループから外す"
   },
   },
+
   "importer_management": {
   "importer_management": {
     "beta_warning": "この機能はベータ版です",
     "beta_warning": "この機能はベータ版です",
-    "import_from": "{{from}} からインポート",
-    "import_growi_archive": "GROWI アーカイブをインポート",
+    "import_from": "%s からインポート",
+    "import_form_growi": "GROWIからインポート",
     "growi_settings": {
     "growi_settings": {
       "overwrite_documents": "インポートされたドキュメントは既存のドキュメントを上書きします",
       "overwrite_documents": "インポートされたドキュメントは既存のドキュメントを上書きします",
-      "growi_archive_file": "GROWI アーカイブファイル",
+      "zip_file": "Zip ファイル",
       "uploaded_data": "アップロードされたデータ",
       "uploaded_data": "アップロードされたデータ",
       "extracted_file": "展開されたファイル",
       "extracted_file": "展開されたファイル",
       "collection": "コレクション",
       "collection": "コレクション",
       "upload": "アップロード",
       "upload": "アップロード",
-      "discard": "アップロードしたデータを破棄する",
-      "errors": {
-        "at_least_one": "コレクションが選択されていません",
-        "page_and_revision": "'Pages' と 'Revisions' はセットでインポートする必要があります",
-        "depends": "'{{condition}}' をインポートする場合は、'{{target}}' を一緒に選択する必要があります"
-      },
-      "configuration": {
-        "pages": {
-          "overwrite_author": {
-            "label": "ページ作成者を現在のユーザーで上書きする",
-            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-          },
-          "set_public_to_page": {
-            "label": "'{{from}}' 設定のページを '公開' 設定にする",
-            "desc": "全ての <b>'{{from}}'</b> 設定のページが<span class=\"text-danger\">全ユーザーから</span>読み取り可能になることに注意してください。"
-          },
-          "initialize_meta_datas": {
-            "label": "「いいね」「閲覧したユーザー」「コメント数」を初期化する",
-            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "HackMD 関連データを初期化する",
-            "desc": "HackMD に重要な下書きデータがない限りはこのオプションをチェックすることを推奨します。"
-          }
-        },
-        "revisions": {
-          "overwrite_author": {
-            "label": "リビジョン作成者を現在のユーザーで上書きする",
-            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-          }
-        }
-      }
+      "discard": "アップロードしたデータを破棄する"
     },
     },
     "esa_settings": {
     "esa_settings": {
       "team_name": "チーム名",
       "team_name": "チーム名",
@@ -788,21 +746,14 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   },
-  "full_text_search_management": {
-    "elasticsearch_management": "Elasticsearch 管理",
-    "build_button": "インデックスのリビルド",
-    "rebuild_description_1": "Build Now ボタンを押すと全てのページのインデックスを削除し、作り直します。",
-    "rebuild_description_2": "この作業には数秒かかります。",
-    "rebuild_description_3": ""
-  },
+
   "export_management": {
   "export_management": {
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
+    "beta_warning": "この機能はベータ版です",
+    "exported_data_list": "エクスポートデータリスト",
     "export_collections": "コレクションのエクスポート",
     "export_collections": "コレクションのエクスポート",
     "check_all": "全てにチェックを付ける",
     "check_all": "全てにチェックを付ける",
     "uncheck_all": "全てからチェックを外す",
     "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
-    "create_new_archive_data": "アーカイブデータの新規作成",
+    "create_new_exported_data": "エクスポートデータの新規作成",
     "export": "エクスポート",
     "export": "エクスポート",
     "cancel": "キャンセル",
     "cancel": "キャンセル",
     "file": "ファイル名",
     "file": "ファイル名",

+ 66 - 96
src/client/js/app.jsx

@@ -34,32 +34,21 @@ import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
 
 
+import CustomCssEditor from './components/Admin/CustomCssEditor';
+import CustomScriptEditor from './components/Admin/CustomScriptEditor';
+import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
+import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
+import ExportPage from './components/Admin/Export/ExportPage';
+import GrowiZipImportSection from './components/Admin/Import/GrowiZipImportSection';
+import GroupDeleteModal from './components/GroupDeleteModal/GroupDeleteModal';
 import ProfileImageUploader from './components/ProfileImageUploader';
 import ProfileImageUploader from './components/ProfileImageUploader';
-import AdminHome from './components/Admin/AdminHome/AdminHome';
-import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
-import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
-import UserManagement from './components/Admin/UserManagement';
-import AppSettingsPage from './components/Admin/App/AppSettingsPage';
-import ManageExternalAccount from './components/Admin/ManageExternalAccount';
-import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
-import Customize from './components/Admin/Customize/Customize';
-import ImportDataPage from './components/Admin/ImportDataPage';
-import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
-import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
 import PageContainer from './services/PageContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import TagContainer from './services/TagContainer';
-import AdminHomeContainer from './services/AdminHomeContainer';
-import AdminCustomizeContainer from './services/AdminCustomizeContainer';
-import UserGroupDetailContainer from './services/UserGroupDetailContainer';
-import AdminUsersContainer from './services/AdminUsersContainer';
-import AdminAppContainer from './services/AdminAppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import WebsocketContainer from './services/WebsocketContainer';
-import AdminMarkDownContainer from './services/AdminMarkDownContainer';
-import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
 
 
 const logger = loggerFactory('growi:app');
 const logger = loggerFactory('growi:app');
 
 
@@ -114,10 +103,7 @@ let componentMappings = {
   'user-created-list': <RecentCreated />,
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
   'user-draft-list': <MyDraftList />,
 
 
-  'admin-full-text-search-management': <FullTextSearchManagement />,
-
   'staff-credit': <StaffCredit />,
   'staff-credit': <StaffCredit />,
-  'admin-importer': <ImportDataPage />,
 };
 };
 
 
 // additional definitions if data exists
 // additional definitions if data exists
@@ -125,17 +111,19 @@ if (pageContainer.state.pageId != null) {
   componentMappings = Object.assign({
   componentMappings = Object.assign({
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     '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 />,
     'revision-toc': <TableOfContents />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
     '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} />,
+
+    'admin-rebuild-search': <AdminRebuildSearch crowi={appContainer} />,
   }, componentMappings);
   }, componentMappings);
 }
 }
 if (pageContainer.state.path != null) {
 if (pageContainer.state.path != null) {
@@ -143,7 +131,7 @@ if (pageContainer.state.path != null) {
     // eslint-disable-next-line quote-props
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
-    'tag-label': <TagLabels />,
+    'tag-label':  <TagLabels />,
   }, componentMappings);
   }, componentMappings);
 }
 }
 
 
@@ -161,91 +149,73 @@ Object.keys(componentMappings).forEach((key) => {
   }
   }
 });
 });
 
 
-// create unstated container instance for admin
-const adminHomeContainer = new AdminHomeContainer(appContainer);
-const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
-const adminUsersContainer = new AdminUsersContainer(appContainer);
-const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
-const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
-const adminContainers = {
-  'admin-home': adminHomeContainer,
-  'admin-customize': adminCustomizeContainer,
-  'admin-user-page': adminUsersContainer,
-  'admin-external-account-setting': adminExternalAccountsContainer,
-  'admin-markdown-setting': adminMarkDownContainer,
-  'admin-export-page': websocketContainer,
-};
-
 // render for admin
 // render for admin
-const adminAppElem = document.getElementById('admin-app');
-if (adminAppElem != null) {
-  const adminAppContainer = new AdminAppContainer(appContainer);
+const customCssEditorElem = document.getElementById('custom-css-editor');
+if (customCssEditorElem != null) {
+  // get input[type=hidden] element
+  const customCssInputElem = document.getElementById('inputCustomCss');
+
   ReactDOM.render(
   ReactDOM.render(
-    <Provider inject={[injectableContainers, adminAppContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <AppSettingsPage />
-      </I18nextProvider>
-    </Provider>,
-    adminAppElem,
+    <CustomCssEditor inputElem={customCssInputElem} />,
+    customCssEditorElem,
   );
   );
 }
 }
+const customScriptEditorElem = document.getElementById('custom-script-editor');
+if (customScriptEditorElem != null) {
+  // get input[type=hidden] element
+  const customScriptInputElem = document.getElementById('inputCustomScript');
 
 
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-const adminComponentMappings = {
-  'admin-home': <AdminHome />,
-  'admin-customize': <Customize />,
-  'admin-user-page': <UserManagement />,
-  'admin-external-account-setting': <ManageExternalAccount />,
-  'admin-markdown-setting': <MarkdownSetting />,
-  'admin-export-page': <ExportArchiveDataPage crowi={appContainer} />,
-};
-
+  ReactDOM.render(
+    <CustomScriptEditor inputElem={customScriptInputElem} />,
+    customScriptEditorElem,
+  );
+}
+const customHeaderEditorElem = document.getElementById('custom-header-editor');
+if (customHeaderEditorElem != null) {
+  // get input[type=hidden] element
+  const customHeaderInputElem = document.getElementById('inputCustomHeader');
 
 
-Object.keys(adminComponentMappings).forEach((key) => {
-  const adminElem = document.getElementById(key);
-  if (adminElem) {
-    ReactDOM.render(
-      <Provider inject={[injectableContainers, adminContainers[key]]}>
-        <I18nextProvider i18n={i18n}>
-          {adminComponentMappings[key]}
-        </I18nextProvider>
-      </Provider>,
-      adminElem,
-    );
-  }
-});
+  ReactDOM.render(
+    <CustomHeaderEditor inputElem={customHeaderInputElem} />,
+    customHeaderEditorElem,
+  );
+}
+const adminGrantSelectorElem = document.getElementById('admin-delete-user-group-modal');
+if (adminGrantSelectorElem != null) {
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <GroupDeleteModal
+        crowi={appContainer}
+      />
+    </I18nextProvider>,
+    adminGrantSelectorElem,
+  );
+}
 
 
-const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
-if (adminUserGroupDetailElem != null) {
-  const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);
+const adminExportPageElem = document.getElementById('admin-export-page');
+if (adminExportPageElem != null) {
   ReactDOM.render(
   ReactDOM.render(
-    <Provider inject={[userGroupDetailContainer]}>
+    <Provider inject={[]}>
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <UserGroupDetailPage />
+        <ExportPage
+          crowi={appContainer}
+        />
       </I18nextProvider>
       </I18nextProvider>
     </Provider>,
     </Provider>,
-    adminUserGroupDetailElem,
+    adminExportPageElem,
   );
   );
 }
 }
 
 
-const adminUserGroupPageElem = document.getElementById('admin-user-group-page');
-if (adminUserGroupPageElem != null) {
-  const isAclEnabled = adminUserGroupPageElem.getAttribute('data-isAclEnabled') === 'true';
-
+// TODO: move to /imponents/Admin/Importer.jsx
+const growiImportElem = document.getElementById('growi-import');
+if (growiImportElem != null) {
   ReactDOM.render(
   ReactDOM.render(
-    <Provider inject={[websocketContainer]}>
+    <Provider inject={[]}>
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <UserGroupPage
-          crowi={appContainer}
-          isAclEnabled={isAclEnabled}
-        />
+        <GrowiZipImportSection />
       </I18nextProvider>
       </I18nextProvider>
     </Provider>,
     </Provider>,
-    adminUserGroupPageElem,
+    growiImportElem,
   );
   );
 }
 }
 
 

+ 0 - 68
src/client/js/components/Admin/AdminHome/AdminHome.jsx

@@ -1,68 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { toastError } from '../../../util/apiNotification';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import AdminHomeContainer from '../../../services/AdminHomeContainer';
-import SystemInfomationTable from './SystemInfomationTable';
-import InstalledPluginTable from './InstalledPluginTable';
-
-const logger = loggerFactory('growi:admin');
-
-class AdminHome extends React.Component {
-
-  async componentDidMount() {
-    const { adminHomeContainer } = this.props;
-
-    try {
-      await adminHomeContainer.retrieveAdminHomeData();
-    }
-    catch (err) {
-      toastError(err);
-      adminHomeContainer.setState({ retrieveError: err });
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <p>
-          {t('admin_top.wiki_administrator')}
-          <br></br>
-          {t('admin_top.assign_administrator')}
-        </p>
-
-        <div className="row mb-5">
-          <h2>{t('admin_top.System Information')}</h2>
-          <SystemInfomationTable />
-        </div>
-
-        <div className="row mb-5">
-          <h2>{t('admin_top.List of installed plugins')}</h2>
-          <InstalledPluginTable />
-
-        </div>
-      </Fragment>
-    );
-  }
-
-}
-
-const AdminHomeWrapper = (props) => {
-  return createSubscribedElement(AdminHome, props, [AppContainer, AdminHomeContainer]);
-};
-
-AdminHome.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-export default withTranslation()(AdminHomeWrapper);

+ 0 - 53
src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import AdminHomeContainer from '../../../services/AdminHomeContainer';
-
-class InstalledPluginTable extends React.Component {
-
-  render() {
-    const { t, adminHomeContainer } = this.props;
-
-    return (
-      <table className="table table-bordered">
-        <thead>
-          <tr>
-            <th className="text-center">{ t('admin_top.Package name') }</th>
-            <th className="text-center">{ t('admin_top.Specified version') }</th>
-            <th className="text-center">{ t('admin_top.Installed version') }</th>
-          </tr>
-        </thead>
-        <tbody>
-          { adminHomeContainer.state.installedPlugins.map((plugin) => {
-            return (
-              <tr key={plugin.name}>
-                <td>{ plugin.name }</td>
-                <td className="text-center">{ plugin.requiredVersion }</td>
-                <td className="text-center">{ plugin.installedVersion }</td>
-              </tr>
-            );
-          }) }
-        </tbody>
-      </table>
-    );
-  }
-
-}
-
-InstalledPluginTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const InstalledPluginTableWrapper = (props) => {
-  return createSubscribedElement(InstalledPluginTable, props, [AppContainer, AdminHomeContainer]);
-};
-
-export default withTranslation()(InstalledPluginTableWrapper);

+ 0 - 53
src/client/js/components/Admin/AdminHome/SystemInfomationTable.jsx

@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import AdminHomeContainer from '../../../services/AdminHomeContainer';
-
-class SystemInformationTable extends React.Component {
-
-  render() {
-    const { adminHomeContainer } = this.props;
-
-    return (
-      <table className="table table-bordered">
-        <tbody>
-          <tr>
-            <th className="col-sm-4">GROWI</th>
-            <td>{ adminHomeContainer.state.growiVersion }</td>
-          </tr>
-          <tr>
-            <th>node.js</th>
-            <td>{ adminHomeContainer.state.nodeVersion }</td>
-          </tr>
-          <tr>
-            <th>npm</th>
-            <td>{ adminHomeContainer.state.npmVersion }</td>
-          </tr>
-          <tr>
-            <th>yarn</th>
-            <td>{ adminHomeContainer.state.yarnVersion }</td>
-          </tr>
-        </tbody>
-      </table>
-    );
-  }
-
-}
-
-SystemInformationTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SystemInformationTableWrapper = (props) => {
-  return createSubscribedElement(SystemInformationTable, props, [AppContainer, AdminHomeContainer]);
-};
-
-export default withTranslation()(SystemInformationTableWrapper);

+ 82 - 0
src/client/js/components/Admin/AdminRebuildSearch.jsx

@@ -0,0 +1,82 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import WebsocketContainer from '../../services/AppContainer';
+
+class AdminRebuildSearch extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isCompleted: false,
+      total: 0,
+      current: 0,
+      skip: 0,
+    };
+  }
+
+  componentDidMount() {
+    const socket = this.props.webspcketContainer.getWebSocket();
+
+    socket.on('admin:addPageProgress', (data) => {
+      const newStates = Object.assign(data, { isCompleted: false });
+      this.setState(newStates);
+    });
+
+    socket.on('admin:finishAddPage', (data) => {
+      const newStates = Object.assign(data, { isCompleted: true });
+      this.setState(newStates);
+    });
+  }
+
+  render() {
+    const {
+      total, current, skip, isCompleted,
+    } = this.state;
+    if (total === 0) {
+      return null;
+    }
+
+    const progressBarLabel = isCompleted ? 'Completed' : `Processing.. ${current}/${total} (${skip} skips)`;
+    const progressBarWidth = isCompleted ? '100%' : `${(current / total) * 100}%`;
+    const progressBarClassNames = isCompleted
+      ? 'progress-bar progress-bar-success'
+      : 'progress-bar progress-bar-striped progress-bar-animated active';
+
+    return (
+      <div>
+        <h5>
+          {progressBarLabel}
+          <span className="pull-right">{progressBarWidth}</span>
+        </h5>
+        <div className="progress progress-sm">
+          <div
+            className={progressBarClassNames}
+            role="progressbar"
+            aria-valuemin="0"
+            aria-valuenow={current}
+            aria-valuemax={total}
+            style={{ width: progressBarWidth }}
+          >
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const AdminRebuildSearchWrapper = (props) => {
+  return createSubscribedElement(AdminRebuildSearch, props, [WebsocketContainer]);
+};
+
+AdminRebuildSearch.propTypes = {
+  webspcketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+};
+
+export default AdminRebuildSearchWrapper;

+ 0 - 139
src/client/js/components/Admin/App/AppSetting.jsx

@@ -1,139 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:appSettings');
-
-class AppSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('app_setting.updated_app_setting'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting.Site Name')}</label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.title}
-              onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
-              placeholder="GROWI"
-            />
-            <p className="help-block">{t('app_setting.sitename_change')}</p>
-          </div>
-        </div>
-
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting.Confidential name')}</label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.confidential}
-              onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
-              placeholder={t('app_setting.ex) internal use only')}
-            />
-            <p className="help-block">{t('app_setting.header_content')}</p>
-          </div>
-        </div>
-
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting.Default Language for new users')}</label>
-          <div className="col-xs-6">
-            <div className="radio radio-primary radio-inline">
-              <input
-                type="radio"
-                id="radioLangEn"
-                name="globalLang"
-                value="en-US"
-                checked={adminAppContainer.state.globalLang === 'en-US'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
-              />
-              <label htmlFor="radioLangEn">{t('English')}</label>
-            </div>
-            <div className="radio radio-primary radio-inline">
-              <input
-                type="radio"
-                id="radioLangJa"
-                name="globalLang"
-                value="ja"
-                checked={adminAppContainer.state.globalLang === 'ja'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
-              />
-              <label htmlFor="radioLangJa">{t('Japanese')}</label>
-            </div>
-          </div>
-        </div>
-
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting.File Uploading')}</label>
-          <div className="col-xs-6">
-            <div className="checkbox checkbox-info">
-              <input
-                type="checkbox"
-                id="cbFileUpload"
-                name="fileUpload"
-                checked={adminAppContainer.state.fileUpload}
-                onChange={(e) => { adminAppContainer.changeFileUpload(e.target.checked) }}
-              />
-              <label htmlFor="cbFileUpload">{t('app_setting.enable_files_except_image')}</label>
-            </div>
-
-            <p className="help-block">
-              {t('app_setting.enable_files_except_image')}
-              <br />
-              {t('app_setting.attach_enable')}
-            </p>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const AppSettingWrapper = (props) => {
-  return createSubscribedElement(AppSetting, props, [AppContainer, AdminAppContainer]);
-};
-
-AppSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(AppSettingWrapper);

+ 0 - 94
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -1,94 +0,0 @@
-import React, { Fragment } from 'react';
-import { withTranslation } from 'react-i18next';
-import PropTypes from 'prop-types';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-
-import AppSetting from './AppSetting';
-import SiteUrlSetting from './SiteUrlSetting';
-import MailSetting from './MailSetting';
-import AwsSetting from './AwsSetting';
-import PluginSetting from './PluginSetting';
-
-const logger = loggerFactory('growi:appSettings');
-
-class AppSettingsPage extends React.Component {
-
-  async componentDidMount() {
-    const { adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.retrieveAppSettingsData();
-    }
-    catch (err) {
-      toastError(err);
-      adminAppContainer.setState({ retrieveError: err });
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <div className="row">
-          <div className="col-md-12">
-            <h2>{t('App Settings')}</h2>
-            <AppSetting />
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="col-md-12">
-            <h2>{t('Site URL settings')}</h2>
-            <SiteUrlSetting />
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="col-md-12">
-            <h2>{t('app_setting.Mail settings')}</h2>
-            <MailSetting />
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="col-md-12">
-            <h2>{t('app_setting.AWS settings')}</h2>
-            <AwsSetting />
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="col-md-12">
-            <h2>{t('app_setting.Plugin settings')}</h2>
-            <PluginSetting />
-          </div>
-        </div>
-      </Fragment>
-    );
-  }
-
-}
-
-AppSettingsPage.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const AppSettingsPageWrapper = (props) => {
-  return createSubscribedElement(AppSettingsPage, props, [AppContainer, AdminAppContainer]);
-};
-
-
-export default withTranslation()(AppSettingsPageWrapper);

+ 0 - 156
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -1,156 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:appSettings');
-
-class AwsSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updateAwsSettingHandler();
-      toastSuccess(t('app_setting.updated_app_setting'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="well">
-          {t('app_setting.AWS_access')}
-          <br />
-          {t('app_setting.No_SMTP_setting')}
-          <br />
-          <br />
-          <span className="text-danger">
-            <i className="ti-unlink"></i>
-            {t('app_setting.change_setting')}
-          </span>
-        </p>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
-            {t('app_setting.region')}
-          </label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              placeholder={`${t('eg')} ap-northeast-1`}
-              defaultValue={adminAppContainer.state.region}
-              onChange={(e) => {
-                adminAppContainer.changeRegion(e.target.value);
-              }}
-            />
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
-            {t('app_setting.custom endpoint')}
-          </label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              placeholder={`${t('eg')} http://localhost:9000`}
-              defaultValue={adminAppContainer.state.customEndpoint}
-              onChange={(e) => {
-                adminAppContainer.changeCustomEndpoint(e.target.value);
-              }}
-            />
-            <p className="help-block">{t('app_setting.custom_endpoint_change')}</p>
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
-            {t('app_setting.bucket name')}
-          </label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              placeholder={`${t('eg')} crowi`}
-              defaultValue={adminAppContainer.state.bucket}
-              onChange={(e) => {
-                adminAppContainer.changeBucket(e.target.value);
-              }}
-            />
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
-            Access Key ID
-          </label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.accessKeyId}
-              onChange={(e) => {
-                adminAppContainer.changeAccessKeyId(e.target.value);
-              }}
-            />
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
-            Secret Access Key
-          </label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.secretKey}
-              onChange={(e) => {
-                adminAppContainer.changeSecretKey(e.target.value);
-              }}
-            />
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const AwsSettingWrapper = (props) => {
-  return createSubscribedElement(AwsSetting, props, [AppContainer, AdminAppContainer]);
-};
-
-AwsSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(AwsSettingWrapper);

+ 0 - 117
src/client/js/components/Admin/App/MailSetting.jsx

@@ -1,117 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:appSettings');
-
-class MailSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('app_setting.updated_app_setting'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="well">{t('app_setting.SMTP_used')} {t('app_setting.SMTP_but_AWS')}<br />{t('app_setting.neihter_of')}</p>
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting.From e-mail address')}</label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="text"
-              placeholder={`${t('eg')} mail@growi.org`}
-              defaultValue={adminAppContainer.state.fromAddress}
-              onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
-            />
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{ t('app_setting.SMTP settings') }</label>
-          <div className="col-xs-4">
-            <label>{ t('app_setting.Host') }</label>
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.smtpHost}
-              onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
-            />
-          </div>
-          <div className="col-xs-2">
-            <label>{ t('app_setting.Port') }</label>
-            <input
-              className="form-control"
-              defaultValue={adminAppContainer.state.smtpPort}
-              onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
-            />
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <div className="col-xs-3 col-xs-offset-3">
-            <label>{ t('app_setting.User') }</label>
-            <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.SmtpUser}
-              onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
-            />
-          </div>
-          <div className="col-xs-3">
-            <label>{ t('Password') }</label>
-            <input
-              className="form-control"
-              type="password"
-              defaultValue={adminAppContainer.state.smtpPassword}
-              onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
-            />
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const MailSettingWrapper = (props) => {
-  return createSubscribedElement(MailSetting, props, [AppContainer, AdminAppContainer]);
-};
-
-MailSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(MailSettingWrapper);

+ 0 - 80
src/client/js/components/Admin/App/PluginSetting.jsx

@@ -1,80 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:app:pluginSetting');
-
-class PluginSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updatePluginSettingHandler();
-      toastSuccess(t('app_setting.updated_plugin_setting'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="well">{t('app_setting.Enable plugin loading')}</p>
-
-        <div className="row mb-5">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
-              <input
-                id="isEnabledPlugins"
-                type="checkbox"
-                checked={adminAppContainer.state.isEnabledPlugins}
-                onChange={(e) => {
-                  adminAppContainer.changeIsEnabledPlugins(e.target.checked);
-                }}
-              />
-              <label htmlFor="isEnabledPlugins">{t('app_setting.Load plugins')}</label>
-            </div>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const PluginSettingWrapper = (props) => {
-  return createSubscribedElement(PluginSetting, props, [AppContainer, AdminAppContainer]);
-};
-
-PluginSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(PluginSettingWrapper);

+ 0 - 108
src/client/js/components/Admin/App/SiteUrlSetting.jsx

@@ -1,108 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminAppContainer from '../../../services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:appSettings');
-
-class SiteUrlSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('app_setting.updated_site_url'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="well">{t('app_setting.Site URL desc')}</p>
-        {!adminAppContainer.state.isSetSiteUrl && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('app_setting.Site URL warn')}</p>)}
-
-        <div className="row">
-          <div className="col-md-12">
-            <div className="col-xs-offset-3">
-              <table className="table settings-table">
-                <colgroup>
-                  <col className="from-db" />
-                  <col className="from-env-vars" />
-                </colgroup>
-                <thead>
-                  <tr>
-                    <th>Database</th>
-                    <th>Environment variables</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr>
-                    <td>
-                      <input
-                        className="form-control"
-                        type="text"
-                        name="settingForm[app:siteUrl]"
-                        defaultValue={adminAppContainer.state.siteUrl}
-                        onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
-                        placeholder="e.g. https://my.growi.org"
-                      />
-                      <p className="help-block">
-                        {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('app_setting.siteurl_help') }} />
-                      </p>
-                    </td>
-                    <td>
-                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl} readOnly />
-                      <p className="help-block">
-                        {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('app_setting.Use env var if empty', { variable: 'APP_SITE_URL' }) }} />
-                      </p>
-                    </td>
-                  </tr>
-                </tbody>
-              </table>
-            </div>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SiteUrlSettingWrapper = (props) => {
-  return createSubscribedElement(SiteUrlSetting, props, [AppContainer, AdminAppContainer]);
-};
-
-SiteUrlSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(SiteUrlSettingWrapper);

+ 0 - 28
src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx

@@ -1,28 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class AdminUpdateButtonRow extends React.PureComponent {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="row my-3">
-        <div className="col-xs-offset-4 col-xs-5">
-          <button type="button" className="btn btn-primary" onClick={this.props.onClick} disabled={this.props.disabled}>{ t('Update') }</button>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-AdminUpdateButtonRow.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  onClick: PropTypes.func.isRequired,
-  disabled: PropTypes.bool.isRequired,
-};
-
-export default withTranslation()(AdminUpdateButtonRow);

+ 0 - 45
src/client/js/components/Admin/Common/ProgressBar.jsx

@@ -1,45 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class ProgressBar extends React.Component {
-
-
-  render() {
-    const {
-      header, currentCount, totalCount, isInProgress,
-    } = this.props;
-
-    const percentage = currentCount / totalCount * 100;
-    const isActive = (isInProgress != null)
-      ? isInProgress //                         apply props.isInProgress if set
-      : (currentCount !== totalCount); //       otherwise, set true when currentCount does not equal totalCount
-
-    return (
-      <>
-        <h5 className="my-1">
-          {header}
-          <div className="pull-right">{currentCount} / {totalCount}</div>
-        </h5>
-        <div className="progress progress-sm">
-          <div
-            className={`progress-bar ${isActive ? 'progress-bar-info progress-bar-striped active' : 'progress-bar-success'}`}
-            style={{ width: `${percentage}%` }}
-          >
-            <span className="sr-only">{percentage.toFixed(0)}% Complete</span>
-          </div>
-        </div>
-      </>
-    );
-  }
-
-}
-
-ProgressBar.propTypes = {
-  header: PropTypes.string.isRequired,
-  currentCount: PropTypes.number.isRequired,
-  totalCount: PropTypes.number.isRequired,
-  isInProgress: PropTypes.bool,
-};
-
-export default withTranslation()(ProgressBar);

+ 5 - 4
src/client/js/components/Admin/CustomCssEditor.jsx

@@ -16,10 +16,12 @@ require('jquery-ui/ui/widgets/resizable');
 export default class CustomCssEditor extends React.Component {
 export default class CustomCssEditor extends React.Component {
 
 
   render() {
   render() {
+    // get initial value from inputElem
+    const value = this.props.inputElem.value;
 
 
     return (
     return (
       <CodeMirror
       <CodeMirror
-        value={this.props.value}
+        value={value}
         autoFocus
         autoFocus
         options={{
         options={{
           mode: 'css',
           mode: 'css',
@@ -41,7 +43,7 @@ export default class CustomCssEditor extends React.Component {
           });
           });
         }}
         }}
         onChange={(editor, data, value) => {
         onChange={(editor, data, value) => {
-          this.props.onChange(value);
+          this.props.inputElem.value = value;
         }}
         }}
       />
       />
     );
     );
@@ -50,6 +52,5 @@ export default class CustomCssEditor extends React.Component {
 }
 }
 
 
 CustomCssEditor.propTypes = {
 CustomCssEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
+  inputElem: PropTypes.object.isRequired,
 };
 };

+ 5 - 4
src/client/js/components/Admin/CustomHeaderEditor.jsx

@@ -14,10 +14,12 @@ require('jquery-ui/ui/widgets/resizable');
 export default class CustomHeaderEditor extends React.Component {
 export default class CustomHeaderEditor extends React.Component {
 
 
   render() {
   render() {
+    // get initial value from inputElem
+    const value = this.props.inputElem.value;
 
 
     return (
     return (
       <CodeMirror
       <CodeMirror
-        value={this.props.value}
+        value={value}
         autoFocus
         autoFocus
         options={{
         options={{
           mode: 'htmlmixed',
           mode: 'htmlmixed',
@@ -39,7 +41,7 @@ export default class CustomHeaderEditor extends React.Component {
           });
           });
         }}
         }}
         onChange={(editor, data, value) => {
         onChange={(editor, data, value) => {
-          this.props.onChange(value);
+          this.props.inputElem.value = value;
         }}
         }}
       />
       />
     );
     );
@@ -48,6 +50,5 @@ export default class CustomHeaderEditor extends React.Component {
 }
 }
 
 
 CustomHeaderEditor.propTypes = {
 CustomHeaderEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
+  inputElem: PropTypes.object.isRequired,
 };
 };

+ 5 - 4
src/client/js/components/Admin/CustomScriptEditor.jsx

@@ -16,10 +16,12 @@ require('jquery-ui/ui/widgets/resizable');
 export default class CustomScriptEditor extends React.Component {
 export default class CustomScriptEditor extends React.Component {
 
 
   render() {
   render() {
+    // get initial value from inputElem
+    const value = this.props.inputElem.value;
 
 
     return (
     return (
       <CodeMirror
       <CodeMirror
-        value={this.props.value}
+        value={value}
         autoFocus
         autoFocus
         options={{
         options={{
           mode: 'javascript',
           mode: 'javascript',
@@ -41,7 +43,7 @@ export default class CustomScriptEditor extends React.Component {
           });
           });
         }}
         }}
         onChange={(editor, data, value) => {
         onChange={(editor, data, value) => {
-          this.props.onChange(value);
+          this.props.inputElem.value = value;
         }}
         }}
       />
       />
     );
     );
@@ -50,6 +52,5 @@ export default class CustomScriptEditor extends React.Component {
 }
 }
 
 
 CustomScriptEditor.propTypes = {
 CustomScriptEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
+  inputElem: PropTypes.object.isRequired,
 };
 };

+ 0 - 85
src/client/js/components/Admin/Customize/Customize.jsx

@@ -1,85 +0,0 @@
-
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastError } from '../../../util/apiNotification';
-
-import CustomizeLayoutSetting from './CustomizeLayoutSetting';
-import CustomizeBehaviorSetting from './CustomizeBehaviorSetting';
-import CustomizeFunctionSetting from './CustomizeFunctionSetting';
-import CustomizeHighlightSetting from './CustomizeHighlightSetting';
-import CustomizeCssSetting from './CustomizeCssSetting';
-import CustomizeScriptSetting from './CustomizeScriptSetting';
-import CustomizeHeaderSetting from './CustomizeHeaderSetting';
-import CustomizeTitle from './CustomizeTitle';
-
-const logger = loggerFactory('growi:Customize');
-
-class Customize extends React.Component {
-
-  async componentDidMount() {
-    const { adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.retrieveCustomizeData();
-    }
-    catch (err) {
-      toastError(err);
-      adminCustomizeContainer.setState({ retrieveError: err });
-      logger.error(err);
-    }
-
-  }
-
-  render() {
-
-    return (
-      <Fragment>
-        <div className="mb-5">
-          <CustomizeLayoutSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeBehaviorSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeFunctionSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeHighlightSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeTitle />
-        </div>
-        <div className="mb-5">
-          <CustomizeHeaderSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeCssSetting />
-        </div>
-        <div className="mb-5">
-          <CustomizeScriptSetting />
-        </div>
-      </Fragment>
-    );
-  }
-
-}
-
-const CustomizeWrapper = (props) => {
-  return createSubscribedElement(Customize, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-Customize.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeWrapper);

+ 0 - 38
src/client/js/components/Admin/Customize/CustomizeBehaviorOption.jsx

@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeBehaviorOption extends React.PureComponent {
-
-  render() {
-
-    return (
-      <React.Fragment>
-        <h4>
-          <div className="radio radio-primary">
-            <input type="radio" id={`radioBehavior${this.props.behaviorType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
-            <label htmlFor={`radioBehavior${this.props.behaviorType}`}>
-              {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
-            </label>
-          </div>
-        </h4>
-        {/* render layout description */}
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeBehaviorOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  behaviorType: PropTypes.string.isRequired,
-  labelHtml: PropTypes.string.isRequired,
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  children: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(CustomizeBehaviorOption);

+ 0 - 99
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -1,99 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import CustomizeBehaviorOption from './CustomizeBehaviorOption';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:Customize');
-
-class CustomizeBehaviorSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeBehavior();
-      toastSuccess(t('customize_page.update_behavior_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Behavior')}</h2>
-        <div className="row">
-          <div className="col-xs-6">
-            <CustomizeBehaviorOption
-              behaviorType="growi"
-              isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
-              onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
-              labelHtml={`GROWI Simplified Behavior <small class="text-success">${t('customize_page.recommended')}</small>`}
-            >
-              <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text2') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text3') }} /></li>
-              </ul>
-            </CustomizeBehaviorOption>
-          </div>
-
-          <div className="col-xs-6">
-            <CustomizeBehaviorOption
-              behaviorType="crowi-plus"
-              isSelected={adminCustomizeContainer.state.currentBehavior === 'crowi-plus'}
-              onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
-              labelHtml="Crowi Classic Behavior"
-            >
-              <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text2') }} /></li>
-                <ul>
-                  <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text3') }} /></li>
-                </ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text4') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text5') }} /></li>
-              </ul>
-            </CustomizeBehaviorOption>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeBehaviorSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeBehaviorSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeBehaviorSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeBehaviorSettingWrapper);

+ 0 - 91
src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -1,91 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomCssEditor from '../CustomCssEditor';
-
-const logger = loggerFactory('growi:Customize');
-
-class CustomizeCssSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      editorInputValue: '',
-    };
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  componentDidMount() {
-    const { customizeCss } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeCss || '' });
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('customize_page.update_customCss_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Custom CSS')}</h2>
-        <p className="well">
-          { t('customize_page.write_CSS') }<br />
-          { t('customize_page.reflect_change') }
-        </p>
-        <div className="form-group">
-          <div className="col-xs-12">
-            <CustomCssEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
-            />
-          </div>
-          <div className="col-xs-12">
-            <p className="help-block text-right">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              { t('customize_page.ctrl_space') }
-            </p>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeCssSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeCssSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeCssSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeCssSettingWrapper);

+ 0 - 38
src/client/js/components/Admin/Customize/CustomizeFunctionOption.jsx

@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeFunctionOption extends React.PureComponent {
-
-  render() {
-    return (
-      <React.Fragment>
-        <div className="checkbox checkbox-success">
-          <input
-            type="checkbox"
-            id={this.props.optionId}
-            checked={this.props.isChecked}
-            onChange={this.props.onChecked}
-          />
-          <label htmlFor={this.props.optionId}>
-            <strong>{this.props.label}</strong>
-          </label>
-        </div>
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeFunctionOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  optionId: PropTypes.string.isRequired,
-  label: PropTypes.string.isRequired,
-  isChecked: PropTypes.bool.isRequired,
-  onChecked: PropTypes.func.isRequired,
-  children: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(CustomizeFunctionOption);

+ 0 - 158
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -1,158 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomizeFunctionOption from './CustomizeFunctionOption';
-
-const logger = loggerFactory('growi:importer');
-
-class CustomizeBehaviorSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('customize_page.update_function_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Function')}</h2>
-        <p className="well">{ t('customize_page.function_choose') }</p>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledTimeline"
-              label={t('customize_page.Timeline function')}
-              isChecked={adminCustomizeContainer.state.isEnabledTimeline}
-              onChecked={() => { adminCustomizeContainer.switchEnableTimeline() }}
-            >
-              <p className="help-block">
-                { t('customize_page.subpage_display') }<br />
-                { t('customize_page.performance_decrease') }<br />
-                { t('customize_page.list_page_display') }
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isSavedStatesOfTabChanges"
-              label={t('customize_page.tab_switch')}
-              isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
-              onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
-            >
-              <p className="help-block">
-                { t('customize_page.save_edit') }<br />
-                { t('customize_page.by_invalidating') }
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledAttachTitleHeader"
-              label={t('customize_page.attach_title_header')}
-              isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
-              onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
-            >
-              <p className="help-block">
-                { t('customize_page.attach_title_header_desc') }
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="my-0 btn-group">
-              <label>{t('customize_page.recent_created__n_draft_num_desc')}</label>
-              <div className="dropdown">
-                <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                  <span className="pull-left">{adminCustomizeContainer.state.currentRecentCreatedLimit}</span>
-                  <span className="bs-caret pull-right">
-                    <span className="caret" />
-                  </span>
-                </button>
-                {/* TODO adjust dropdown after BS4 */}
-                <ul className="dropdown-menu" role="menu">
-                  <li key={10} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(10) }}>
-                    <a role="menuitem">10</a>
-                  </li>
-                  <li key={30} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(30) }}>
-                    <a role="menuitem">30</a>
-                  </li>
-                  <li key={50} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(50) }}>
-                    <a role="menuitem">50</a>
-                  </li>
-                </ul>
-              </div>
-              <p className="help-block">
-                { t('customize_page.recently_created_n_draft_num_desc') }
-              </p>
-            </div>
-          </div>
-        </div>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledStaleNotification"
-              label={t('customize_page.stale_notification')}
-              isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
-              onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
-            >
-              <p className="help-block">
-                { t('customize_page.stale_notification_desc') }
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeBehaviorSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeBehaviorSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeBehaviorSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeBehaviorSettingWrapper);

+ 0 - 100
src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx

@@ -1,100 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomHeaderEditor from '../CustomHeaderEditor';
-
-const logger = loggerFactory('growi:Customize');
-
-class CustomizeHeaderSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      editorInputValue: '',
-    };
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  componentDidMount() {
-    const { customizeHeader } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeHeader || '' });
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('customize_page.update_customHeader_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.custom_header')}</h2>
-
-        <p
-          className="well"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('customize_page.custom_header_detail') }}
-        />
-
-        <div className="help-block">
-          { t('Example') }:
-          <pre className="hljs">
-            {/* eslint-disable-next-line react/no-unescaped-entities */}
-            <code>&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js" defer&gt;&lt;/script&gt;</code>
-          </pre>
-        </div>
-
-        <div className="col-xs-12">
-          <CustomHeaderEditor
-            // The value passed must be immutable
-            value={this.state.editorInputValue}
-            onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
-          />
-        </div>
-        <div className="col-xs-12">
-          <p className="help-block text-right">
-            <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-            { t('customize_page.ctrl_space') }
-          </p>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeHeaderSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeHeaderSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeHeaderSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeHeaderSettingWrapper);

+ 0 - 143
src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -1,143 +0,0 @@
-/* eslint-disable no-useless-escape */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:customizeHighlight');
-
-class CustomizeHighlightSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateHighlightJsStyle();
-      toastSuccess(t('customize_page.update_highlight_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  getDemoFunction() {
-    return `function $initHighlight(block, cls) {
-    try {
-
-      if (cls.search(/\bno\-highlight\b/) !== -1) {
-        return \`\${process(block, true, 0x0F)} class="\${cls}"\`;
-      }
-    }
-    catch (e) {
-      /* handle exception */
-    }
-    for (let i = 0 / 2; i < classes.length; i++) {
-      if (checkCondition(classes[i]) === undefined) { console.log('undefined') }
-    }
-  };`;
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-    const options = adminCustomizeContainer.state.highlightJsCssSelectorOptions;
-    const menuItem = [];
-
-    Object.entries(options).forEach((option) => {
-      const styleId = option[0];
-      const styleName = option[1].name;
-      const isBorderEnable = option[1].border;
-
-      menuItem.push(
-        <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
-          <a role="menuitem">{styleName}</a>
-        </li>,
-      );
-    });
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Code Highlight')}</h2>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="my-0 btn-group">
-              <label>{t('customize_page.Theme')}</label>
-              <div className="dropdown">
-                <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                  <span className="pull-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
-                  <span className="bs-caret pull-right">
-                    <span className="caret" />
-                  </span>
-                </button>
-                {/* TODO adjust dropdown after BS4 */}
-                <ul className="dropdown-menu" role="menu">
-                  {menuItem}
-                </ul>
-              </div>
-              {/* eslint-disable-next-line react/no-danger */}
-              <p className="help-block text-warning"><span dangerouslySetInnerHTML={{ __html:  t('customize_page.nocdn_desc') }} /></p>
-            </div>
-          </div>
-        </div>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
-              <input
-                type="checkbox"
-                id="highlightBorder"
-                checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
-                onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
-              />
-              <label htmlFor="highlightBorder">
-                <strong>Border</strong>
-              </label>
-            </div>
-          </div>
-        </div>
-
-        <div className="help-block">
-          <label>Examples:</label>
-          <div className="wiki">
-            <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-              <code className="highlightjs-demo">
-                {this.getDemoFunction()}
-              </code>
-            </pre>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeHighlightSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeHighlightSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeHighlightSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeHighlightSettingWrapper);

+ 0 - 42
src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx

@@ -1,42 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeLayoutOption extends React.Component {
-
-  render() {
-    const { layoutType } = this.props;
-
-    return (
-      <React.Fragment>
-        <h4>
-          <div className="radio radio-primary">
-            <input type="radio" id={`radio-layout-${layoutType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
-            <label htmlFor={`radio-layout-${layoutType}`}>
-              {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
-            </label>
-          </div>
-        </h4>
-        <a href={`/images/admin/customize/layout-${layoutType}.gif`} className="ss-container">
-          <img src={`/images/admin/customize/layout-${layoutType}-thumb.gif`} width="240px" />
-        </a>
-        {/* render layout description */}
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeLayoutOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  layoutType: PropTypes.string.isRequired,
-  labelHtml: PropTypes.string.isRequired,
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  children: PropTypes.array.isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutOption);

+ 0 - 81
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -1,81 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AppContainer from '../../../services/AppContainer';
-
-import CustomizeLayoutOption from './CustomizeLayoutOption';
-
-class CustomizeLayoutOptions extends React.Component {
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <div className="row">
-        <div className="col-sm-4">
-          <CustomizeLayoutOption
-            layoutType="crowi-plus"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-            labelHtml={`GROWI Enhanced Layout <small class="text-success">${t('customize_page.recommended')}</small>`}
-          >
-            <h4>{t('customize_page.layout_description.growi_title')}</h4>
-            <ul>
-              <li>{t('customize_page.layout_description.growi_text1')}</li>
-              <li>{t('customize_page.layout_description.growi_text2')}</li>
-              <li>{t('customize_page.layout_description.growi_text3')}</li>
-            </ul>
-          </CustomizeLayoutOption>
-        </div>
-
-        <div className="col-sm-4">
-          <CustomizeLayoutOption
-            layoutType="kibela"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
-            labelHtml="Kibela Like Layout"
-          >
-            <h4>{t('customize_page.layout_description.kibela_title')}</h4>
-            <ul>
-              <li>{t('customize_page.layout_description.kibela_text1')}</li>
-              <li>{t('customize_page.layout_description.kibela_text2')}</li>
-              <li>{t('customize_page.layout_description.kibela_text3')}</li>
-            </ul>
-          </CustomizeLayoutOption>
-        </div>
-
-        <div className="col-sm-4">
-          <CustomizeLayoutOption
-            layoutType="classic"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'crowi'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('crowi')}
-            labelHtml="Crowi Classic Layout"
-          >
-            <h4>{t('customize_page.layout_description.crowi_title')}</h4>
-            <ul>
-              <li>{t('customize_page.layout_description.crowi_text1')}</li>
-              <li>{t('customize_page.layout_description.crowi_text2')}</li>
-              <li>{t('customize_page.layout_description.crowi_text3')}</li>
-            </ul>
-          </CustomizeLayoutOption>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-const CustomizeLayoutOptionsWrapper = (props) => {
-  return createSubscribedElement(CustomizeLayoutOptions, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeLayoutOptions.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutOptionsWrapper);

+ 0 - 79
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -1,79 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import CustomizeLayoutOptions from './CustomizeLayoutOptions';
-import CustomizeThemeOptions from './CustomizeThemeOptions';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:importer');
-
-
-class CustomizeLayoutSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeLayoutAndTheme();
-      toastSuccess(t('customize_page.update_layout_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  renderDevAlert() {
-    if (process.env.NODE_ENV === 'development') {
-      return (
-        <div className="alert alert-warning">
-          <strong>DEBUG MESSAGE:</strong> development build では、リアルタイムプレビューが無効になります
-        </div>
-      );
-    }
-  }
-
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Layout')}</h2>
-        <CustomizeLayoutOptions />
-        <h2 className="admin-setting-header">{t('customize_page.Theme')}</h2>
-        {this.renderDevAlert()}
-        <CustomizeThemeOptions />
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeLayoutSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeLayoutSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeLayoutSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutSettingWrapper);

+ 0 - 122
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -1,122 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomScriptEditor from '../CustomScriptEditor';
-
-const logger = loggerFactory('growi:customizeScript');
-
-class CustomizeScriptSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      editorInputValue: '',
-    };
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  componentDidMount() {
-    const { customizeScript } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeScript || '' });
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('customize_page.update_script_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  getExampleCode() {
-    return `console.log($('.main-container'));
-    window.addEventListener('load', (event) => {
-      console.log('config: ', appContainer.config);
-    });
-    `;
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.Custom script')}</h2>
-        <p className="well">
-          { t('customize_page.write_java') }<br />
-          { t('customize_page.reflect_change') }
-        </p>
-
-        <div className="help-block">
-          Placeholders:<br />
-          (Available after <code>load</code> event)
-          <dl className="dl-horizontal">
-            <dt><code>$</code></dt>
-            <dd>jQuery instance</dd>
-            <dt><code>appContainer</code></dt>
-            <dd>GROWI App <a href="https://github.com/jamiebuilds/unstated">Unstated Container</a></dd>
-            <dt><code>growiRenderer</code></dt>
-            <dd>GROWI Renderer origin instance</dd>
-            <dt><code>growiPlugin</code></dt>
-            <dd>GROWI Plugin Manager instance</dd>
-            <dt><code>Crowi</code></dt>
-            <dd>Crowi legacy instance (jQuery based)</dd>
-          </dl>
-        </div>
-
-        <div className="help-block">
-          Examples:
-          <pre className="hljs"><code>{this.getExampleCode()}</code></pre>
-        </div>
-
-        <div className="form-group">
-          <div className="col-xs-12">
-            <CustomScriptEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
-            />
-          </div>
-          <div className="col-xs-12">
-            <p className="help-block text-right">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              { t('customize_page.ctrl_space') }
-            </p>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeScriptSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeScriptSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeScriptSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeScriptSettingWrapper);

+ 0 - 91
src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -1,91 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-
-import AppContainer from '../../../services/AppContainer';
-import ThemeColorBox from './ThemeColorBox';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-
-class CustomizeThemeOptions extends React.Component {
-
-  render() {
-    const { adminCustomizeContainer } = this.props;
-    const { currentLayout, currentTheme } = adminCustomizeContainer.state;
-
-    const lightTheme = [{
-      name: 'default', bg: '#ffffff', topbar: '#334455', theme: '#112744',
-    }, {
-      name: 'nature', bg: '#f9fff3', topbar: '#118050', theme: '#460039',
-    }, {
-      name: 'mono-blue', bg: '#F7FBFD', topbar: '#00587A', theme: '#00587A',
-    }, {
-      name: 'wood', bg: '#fffefb', topbar: '#aaa45f', theme: '#dddebf',
-    }, {
-      name: 'island', bg: '#8ecac0', topbar: '#0c2a44', theme: '#cef2ef',
-    }, {
-      name: 'christmas', bg: '#fffefb', topbar: '#b3000c', theme: '#017e20',
-    }, {
-      name: 'antarctic', bg: '#ffffff', topbar: '#000080', theme: '#99cccc',
-    }];
-
-    const darkTheme = [{
-      name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36',
-    }, {
-      name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
-    }, {
-      name: 'blue-night', bg: '#061F2F', topbar: '#27343B', theme: '#0090C8',
-    }, {
-      name: 'halloween', bg: '#030003', topbar: '#cc5d1f', theme: '#e9af2b',
-    }];
-
-    return (
-      <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
-        {/* Light Themes  */}
-        <div className="d-flex">
-          {lightTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                name={theme.name}
-                bg={theme.bg}
-                topbar={theme.topbar}
-                theme={theme.theme}
-              />
-            );
-          })}
-        </div>
-        {/* Dark Themes  */}
-        <div className="d-flex mt-3">
-          {darkTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                name={theme.name}
-                bg={theme.bg}
-                topbar={theme.topbar}
-                theme={theme.theme}
-              />
-            );
-          })}
-        </div>
-      </div>
-    );
-  }
-
-}
-
-const CustomizeThemeOptionsWrapper = (props) => {
-  return createSubscribedElement(CustomizeThemeOptions, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeThemeOptions.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default CustomizeThemeOptionsWrapper;

+ 0 - 79
src/client/js/components/Admin/Customize/CustomizeTitle.jsx

@@ -1,79 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import loggerFactory from '@alias/logger';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-const logger = loggerFactory('growi:Customize');
-
-class CustomizeTitle extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeTitle();
-      toastSuccess(t('customize_page.update_customTitle_success'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-    const { currentCustomizeTitle } = adminCustomizeContainer.state;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_page.custom_title')}</h2>
-        <p
-          className="well"
-          // eslint-disable-next-line react/no-danger, max-len
-          dangerouslySetInnerHTML={{ __html: '<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br><code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます。' }}
-        />
-        {/* TODO i18n */}
-        <div className="help-block">
-          Default Value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
-          <br />
-          Default Output: <pre><code className="xml">&lt;title&gt;/Sandbox - {'GROWI'}&lt;&#047;title&gt;</code></pre>
-        </div>
-        <div className="form-group">
-          <input
-            className="form-control"
-            defaultValue={currentCustomizeTitle}
-            onChange={(e) => { adminCustomizeContainer.changeCustomizeTitle(e.target.value) }}
-          />
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeTitleWrapper = (props) => {
-  return createSubscribedElement(CustomizeTitle, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeTitle.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeTitleWrapper);

+ 0 - 45
src/client/js/components/Admin/Customize/ThemeColorBox.jsx

@@ -1,45 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-
-class ThemeColorBox extends React.PureComponent {
-
-  render() {
-    const { name } = this.props;
-
-    return (
-      <div
-        id={`theme-option-${name}`}
-        className={`theme-option-container d-flex flex-column align-items-center ${this.props.isSelected && 'active'}`}
-        onClick={this.props.onSelected}
-      >
-        <a
-          className={`m-0 ${name} theme-button`}
-          id={name}
-        >
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
-            <g>
-              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={this.props.bg}></path>
-              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={this.props.topbar}></path>
-              <path d="M 44 15 L65 15 L65 65 L44 65 L44 15 Z" fill={this.props.theme}></path>
-            </g>
-          </svg>
-        </a>
-        <span className="theme-option-name"><b>{ name }</b></span>
-      </div>
-    );
-  }
-
-}
-
-
-ThemeColorBox.propTypes = {
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  name: PropTypes.string.isRequired,
-  bg: PropTypes.string.isRequired,
-  topbar: PropTypes.string.isRequired,
-  theme: PropTypes.string.isRequired,
-};
-
-export default ThemeColorBox;

+ 138 - 0
src/client/js/components/Admin/Export/ExportPage.jsx

@@ -0,0 +1,138 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import * as toastr from 'toastr';
+
+import ExportZipFormModal from './ExportZipFormModal';
+import ZipFileTable from './ZipFileTable';
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+// import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class ExportPage extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      collections: [],
+      zipFileStats: [],
+      isExportModalOpen: false,
+    };
+
+    this.onZipFileStatAdd = this.onZipFileStatAdd.bind(this);
+    this.onZipFileStatRemove = this.onZipFileStatRemove.bind(this);
+    this.openExportModal = this.openExportModal.bind(this);
+    this.closeExportModal = this.closeExportModal.bind(this);
+  }
+
+  async componentDidMount() {
+    // TODO:: use apiv3.get
+    // eslint-disable-next-line no-unused-vars
+    const [{ collections }, { zipFileStats }] = await Promise.all([
+      this.props.appContainer.apiGet('/v3/mongo/collections', {}),
+      this.props.appContainer.apiGet('/v3/export/status', {}),
+    ]);
+    // TODO: toastSuccess, toastError
+
+    this.setState({ collections: ['pages', 'revisions'], zipFileStats }); // FIXME: delete this line and uncomment the line below
+    // this.setState({ collections, zipFileStats });
+  }
+
+  onZipFileStatAdd(newStat) {
+    this.setState((prevState) => {
+      return {
+        zipFileStats: [...prevState.zipFileStats, newStat],
+      };
+    });
+  }
+
+  async onZipFileStatRemove(fileName) {
+    try {
+      await this.props.appContainer.apiDelete(`/v3/export/${fileName}`, {});
+
+      this.setState((prevState) => {
+        return {
+          zipFileStats: prevState.zipFileStats.filter(stat => stat.fileName !== fileName),
+        };
+      });
+
+      // TODO: toastSuccess, toastError
+      toastr.success(undefined, `Deleted ${fileName}`, {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '1200',
+        extendedTimeOut: '150',
+      });
+    }
+    catch (err) {
+      // TODO: toastSuccess, toastError
+      toastr.error(err, 'Error', {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '3000',
+      });
+    }
+  }
+
+  openExportModal() {
+    this.setState({ isExportModalOpen: true });
+  }
+
+  closeExportModal() {
+    this.setState({ isExportModalOpen: false });
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <div className="alert alert-warning">
+          <i className="icon-exclamation"></i> { t('export_management.beta_warning') }
+        </div>
+
+        <h2>{t('Export Data')}</h2>
+
+        <button type="button" className="btn btn-default" onClick={this.openExportModal}>{t('export_management.create_new_exported_data')}</button>
+
+        <div className="mt-5">
+          <h3>{t('export_management.exported_data_list')}</h3>
+          <ZipFileTable
+            zipFileStats={this.state.zipFileStats}
+            onZipFileStatRemove={this.onZipFileStatRemove}
+          />
+        </div>
+
+        <ExportZipFormModal
+          isOpen={this.state.isExportModalOpen}
+          onClose={this.closeExportModal}
+          collections={this.state.collections}
+          zipFileStats={this.state.zipFileStats}
+          onZipFileStatAdd={this.onZipFileStatAdd}
+        />
+      </Fragment>
+    );
+  }
+
+}
+
+ExportPage.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ExportPageFormWrapper = (props) => {
+  return createSubscribedElement(ExportPage, props, [AppContainer]);
+};
+
+export default withTranslation()(ExportPageFormWrapper);

+ 5 - 5
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx → src/client/js/components/Admin/Export/ExportTableMenu.jsx

@@ -6,7 +6,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
-class ArchiveFilesTableMenu extends React.Component {
+class ExportTableMenu extends React.Component {
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
@@ -35,7 +35,7 @@ class ArchiveFilesTableMenu extends React.Component {
 
 
 }
 }
 
 
-ArchiveFilesTableMenu.propTypes = {
+ExportTableMenu.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   fileName: PropTypes.string.isRequired,
   fileName: PropTypes.string.isRequired,
@@ -45,8 +45,8 @@ ArchiveFilesTableMenu.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ArchiveFilesTableMenuWrapper = (props) => {
-  return createSubscribedElement(ArchiveFilesTableMenu, props, [AppContainer]);
+const ExportTableMenuWrapper = (props) => {
+  return createSubscribedElement(ExportTableMenu, props, [AppContainer]);
 };
 };
 
 
-export default withTranslation()(ArchiveFilesTableMenuWrapper);
+export default withTranslation()(ExportTableMenuWrapper);

+ 162 - 0
src/client/js/components/Admin/Export/ExportZipFormModal.jsx

@@ -0,0 +1,162 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import Modal from 'react-bootstrap/es/Modal';
+import * as toastr from 'toastr';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+// import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class ExportZipFormModal extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      collections: new Set(),
+    };
+
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
+    this.checkAll = this.checkAll.bind(this);
+    this.uncheckAll = this.uncheckAll.bind(this);
+    this.export = this.export.bind(this);
+    this.validateForm = this.validateForm.bind(this);
+  }
+
+  toggleCheckbox(e) {
+    const { target } = e;
+    const { name, checked } = target;
+
+    this.setState((prevState) => {
+      const collections = new Set(prevState.collections);
+      if (checked) {
+        collections.add(name);
+      }
+      else {
+        collections.delete(name);
+      }
+
+      return { collections };
+    });
+  }
+
+  checkAll() {
+    this.setState({ collections: new Set(this.props.collections) });
+  }
+
+  uncheckAll() {
+    this.setState({ collections: new Set() });
+  }
+
+  async export(e) {
+    e.preventDefault();
+
+    try {
+      // TODO: use appContainer.apiv3.post
+      const { zipFileStat } = await this.props.appContainer.apiPost('/v3/export', { collections: Array.from(this.state.collections) });
+      // TODO: toastSuccess, toastError
+      this.props.onZipFileStatAdd(zipFileStat);
+      this.props.onClose();
+
+      // TODO: toastSuccess, toastError
+      toastr.success(undefined, `Generated ${zipFileStat.fileName}`, {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '1200',
+        extendedTimeOut: '150',
+      });
+    }
+    catch (err) {
+      // TODO: toastSuccess, toastError
+      toastr.error(err, 'Error', {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '3000',
+      });
+    }
+  }
+
+  validateForm() {
+    return this.state.collections.size > 0;
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
+        <Modal.Header closeButton>
+          <Modal.Title>{t('export_management.export_collections')}</Modal.Title>
+        </Modal.Header>
+
+        <form onSubmit={this.export}>
+          <Modal.Body>
+            <div className="row">
+              <div className="col-sm-12">
+                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
+                  <i className="fa fa-check-square-o"></i> {t('export_management.check_all')}
+                </button>
+                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
+                  <i className="fa fa-square-o"></i> {t('export_management.uncheck_all')}
+                </button>
+              </div>
+            </div>
+            <div className="checkbox checkbox-info">
+              {this.props.collections.map((collectionName) => {
+                return (
+                  <div className="my-1" key={collectionName}>
+                    <input
+                      type="checkbox"
+                      id={collectionName}
+                      name={collectionName}
+                      className="form-check-input"
+                      value={collectionName}
+                      checked={this.state.collections.has(collectionName)}
+                      onChange={this.toggleCheckbox}
+                    />
+                    <label className="text-capitalize form-check-label ml-3" htmlFor={collectionName}>
+                      {collectionName}
+                    </label>
+                  </div>
+                );
+              })}
+            </div>
+          </Modal.Body>
+
+          <Modal.Footer>
+            <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
+            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management.export')}</button>
+          </Modal.Footer>
+        </form>
+      </Modal>
+    );
+  }
+
+}
+
+ExportZipFormModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  collections: PropTypes.arrayOf(PropTypes.string).isRequired,
+  zipFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
+  onZipFileStatAdd: PropTypes.func.isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ExportZipFormModalWrapper = (props) => {
+  return createSubscribedElement(ExportZipFormModal, props, [AppContainer]);
+};
+
+export default withTranslation()(ExportZipFormModalWrapper);

+ 9 - 10
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx → src/client/js/components/Admin/Export/ZipFileTable.jsx

@@ -3,13 +3,12 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
+import ExportTableMenu from './ExportTableMenu';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
-import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
-
-class ArchiveFilesTable extends React.Component {
+class ZipFileTable extends React.Component {
 
 
   render() {
   render() {
     // eslint-disable-next-line no-unused-vars
     // eslint-disable-next-line no-unused-vars
@@ -27,15 +26,15 @@ class ArchiveFilesTable extends React.Component {
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-          {this.props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
+          {this.props.zipFileStats.map(({ meta, fileName, fileStats }) => {
             return (
             return (
               <tr key={fileName}>
               <tr key={fileName}>
                 <th>{fileName}</th>
                 <th>{fileName}</th>
                 <td>{meta.version}</td>
                 <td>{meta.version}</td>
-                <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
+                <td className="text-capitalize">{fileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
                 <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
                 <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
                 <td>
                 <td>
-                  <ArchiveFilesTableMenu
+                  <ExportTableMenu
                     fileName={fileName}
                     fileName={fileName}
                     onZipFileStatRemove={this.props.onZipFileStatRemove}
                     onZipFileStatRemove={this.props.onZipFileStatRemove}
                   />
                   />
@@ -50,7 +49,7 @@ class ArchiveFilesTable extends React.Component {
 
 
 }
 }
 
 
-ArchiveFilesTable.propTypes = {
+ZipFileTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
@@ -61,8 +60,8 @@ ArchiveFilesTable.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ArchiveFilesTableWrapper = (props) => {
-  return createSubscribedElement(ArchiveFilesTable, props, [AppContainer]);
+const ZipFileTableWrapper = (props) => {
+  return createSubscribedElement(ZipFileTable, props, [AppContainer]);
 };
 };
 
 
-export default withTranslation()(ArchiveFilesTableWrapper);
+export default withTranslation()(ZipFileTableWrapper);

+ 0 - 245
src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -1,245 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
-import * as toastr from 'toastr';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-// import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
-const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
-];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
-
-class SelectCollectionsModal extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      selectedCollections: new Set(),
-    };
-
-    this.toggleCheckbox = this.toggleCheckbox.bind(this);
-    this.checkAll = this.checkAll.bind(this);
-    this.uncheckAll = this.uncheckAll.bind(this);
-    this.export = this.export.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  toggleCheckbox(e) {
-    const { target } = e;
-    const { name, checked } = target;
-
-    this.setState((prevState) => {
-      const selectedCollections = new Set(prevState.selectedCollections);
-      if (checked) {
-        selectedCollections.add(name);
-      }
-      else {
-        selectedCollections.delete(name);
-      }
-
-      return { selectedCollections };
-    });
-  }
-
-  checkAll() {
-    this.setState({ selectedCollections: new Set(this.props.collections) });
-  }
-
-  uncheckAll() {
-    this.setState({ selectedCollections: new Set() });
-  }
-
-  async export(e) {
-    e.preventDefault();
-
-    try {
-      // TODO: use appContainer.apiv3.post
-      const result = await this.props.appContainer.apiPost('/v3/export', { collections: Array.from(this.state.selectedCollections) });
-      // TODO: toastSuccess, toastError
-
-      if (!result.ok) {
-        throw new Error('Error occured.');
-      }
-
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, 'Export process has requested.', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
-
-      this.props.onExportingRequested();
-      this.props.onClose();
-
-      this.setState({ selectedCollections: new Set() });
-
-    }
-    catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
-    }
-  }
-
-  validateForm() {
-    return this.state.selectedCollections.size > 0;
-  }
-
-  renderWarnForUser() {
-    // whether this.state.selectedCollections includes one of GROUPS_USER
-    const isUserRelatedDataSelected = GROUPS_USER.some((collectionName) => {
-      return this.state.selectedCollections.has(collectionName);
-    });
-
-    if (!isUserRelatedDataSelected) {
-      return <></>;
-    }
-
-    const html = this.props.t('export_management.desc_password_seed');
-
-    // eslint-disable-next-line react/no-danger
-    return <div className="well well-sm" dangerouslySetInnerHTML={{ __html: html }}></div>;
-  }
-
-  renderGroups(groupList, color) {
-    const collectionNames = groupList.filter((collectionName) => {
-      return this.props.collections.includes(collectionName);
-    });
-
-    return this.renderCheckboxes(collectionNames, color);
-  }
-
-  renderOthers() {
-    const collectionNames = this.props.collections.filter((collectionName) => {
-      return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
-    });
-
-    return this.renderCheckboxes(collectionNames);
-  }
-
-  renderCheckboxes(collectionNames, color) {
-    const checkboxColor = color ? `checkbox-${color}` : 'checkbox-info';
-
-    return (
-      <div className={`row checkbox ${checkboxColor}`}>
-        {collectionNames.map((collectionName) => {
-          return (
-            <div className="col-xs-6 my-1" key={collectionName}>
-              <input
-                type="checkbox"
-                id={collectionName}
-                name={collectionName}
-                className="form-check-input"
-                value={collectionName}
-                checked={this.state.selectedCollections.has(collectionName)}
-                onChange={this.toggleCheckbox}
-              />
-              <label className="text-capitalize form-check-label ml-3" htmlFor={collectionName}>
-                {collectionName}
-              </label>
-            </div>
-          );
-        })}
-      </div>
-    );
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header closeButton>
-          <Modal.Title>{t('export_management.export_collections')}</Modal.Title>
-        </Modal.Header>
-
-        <form onSubmit={this.export}>
-          <Modal.Body>
-            <div className="row">
-              <div className="col-sm-12">
-                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
-                  <i className="fa fa-check-square-o"></i> {t('export_management.check_all')}
-                </button>
-                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
-                  <i className="fa fa-square-o"></i> {t('export_management.uncheck_all')}
-                </button>
-              </div>
-            </div>
-            <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>Page Collections</legend>
-                { this.renderGroups(GROUPS_PAGE) }
-              </div>
-            </div>
-            <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>User Collections</legend>
-                { this.renderGroups(GROUPS_USER, 'danger') }
-                { this.renderWarnForUser() }
-              </div>
-            </div>
-            <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>Config Collections</legend>
-                { this.renderGroups(GROUPS_CONFIG) }
-              </div>
-            </div>
-            <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>Other Collections</legend>
-                { this.renderOthers() }
-              </div>
-            </div>
-          </Modal.Body>
-
-          <Modal.Footer>
-            <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
-            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management.export')}</button>
-          </Modal.Footer>
-        </form>
-      </Modal>
-    );
-  }
-
-}
-
-SelectCollectionsModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isOpen: PropTypes.bool.isRequired,
-  onExportingRequested: PropTypes.func.isRequired,
-  onClose: PropTypes.func.isRequired,
-  collections: PropTypes.arrayOf(PropTypes.string).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SelectCollectionsModalWrapper = (props) => {
-  return createSubscribedElement(SelectCollectionsModal, props, [AppContainer]);
-};
-
-export default withTranslation()(SelectCollectionsModalWrapper);

+ 0 - 261
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -1,261 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import * as toastr from 'toastr';
-
-
-import { createSubscribedElement } from '../UnstatedUtils';
-// import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../services/AppContainer';
-import WebsocketContainer from '../../services/WebsocketContainer';
-
-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) {
-    super(props);
-
-    this.state = {
-      collections: [],
-      zipFileStats: [],
-      progressList: [],
-      isExportModalOpen: false,
-      isExporting: false,
-      isZipping: false,
-      isExported: false,
-    };
-
-    this.onZipFileStatAdd = this.onZipFileStatAdd.bind(this);
-    this.onZipFileStatRemove = this.onZipFileStatRemove.bind(this);
-    this.openExportModal = this.openExportModal.bind(this);
-    this.closeExportModal = this.closeExportModal.bind(this);
-    this.exportingRequestedHandler = this.exportingRequestedHandler.bind(this);
-  }
-
-  async componentWillMount() {
-    // TODO:: use apiv3.get
-    // eslint-disable-next-line no-unused-vars
-    const [{ collections }, { status }] = await Promise.all([
-      this.props.appContainer.apiGet('/v3/mongo/collections', {}),
-      this.props.appContainer.apiGet('/v3/export/status', {}),
-    ]);
-    // 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: filteredCollections,
-      zipFileStats,
-      isExporting,
-      progressList,
-    });
-
-    this.setupWebsocketEventHandler();
-  }
-
-  setupWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
-
-    // websocket event
-    socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
-      this.setState({
-        isExporting: true,
-        progressList,
-      });
-    });
-
-    // websocket event
-    socket.on('admin:onStartZippingForExport', () => {
-      this.setState({
-        isZipping: true,
-      });
-    });
-
-    // websocket event
-    socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
-      const zipFileStats = this.state.zipFileStats.concat([addedZipFileStat]);
-
-      this.setState({
-        isExporting: false,
-        isZipping: false,
-        isExported: true,
-        zipFileStats,
-      });
-
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
-    });
-  }
-
-  onZipFileStatAdd(newStat) {
-    this.setState((prevState) => {
-      return {
-        zipFileStats: [...prevState.zipFileStats, newStat],
-      };
-    });
-  }
-
-  async onZipFileStatRemove(fileName) {
-    try {
-      await this.props.appContainer.apiDelete(`/v3/export/${fileName}`, {});
-
-      this.setState((prevState) => {
-        return {
-          zipFileStats: prevState.zipFileStats.filter(stat => stat.fileName !== fileName),
-        };
-      });
-
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `Deleted ${fileName}`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
-    }
-    catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
-    }
-  }
-
-  openExportModal() {
-    this.setState({ isExportModalOpen: true });
-  }
-
-  closeExportModal() {
-    this.setState({ isExportModalOpen: false });
-  }
-
-  /**
-   * event handler invoked when export process was requested successfully
-   */
-  exportingRequestedHandler() {
-  }
-
-  renderProgressBarsForCollections() {
-    const cols = this.state.progressList.map((progressData) => {
-      const { collectionName, currentCount, totalCount } = progressData;
-      return (
-        <div className="col-md-6" key={collectionName}>
-          <ProgressBar
-            header={collectionName}
-            currentCount={currentCount}
-            totalCount={totalCount}
-          />
-        </div>
-      );
-    });
-
-    return <div className="row px-3">{cols}</div>;
-  }
-
-  renderProgressBarForZipping() {
-    const { isZipping, isExported } = this.state;
-    const showZippingBar = isZipping || isExported;
-
-    if (!showZippingBar) {
-      return <></>;
-    }
-
-    return (
-      <div className="row px-3">
-        <div className="col-md-12" key="progressBarForZipping">
-          <ProgressBar
-            header="Zip Files"
-            currentCount={1}
-            totalCount={1}
-            isInProgress={isZipping}
-          />
-        </div>
-      </div>
-    );
-  }
-
-  render() {
-    const { t } = this.props;
-    const { isExporting, isExported, progressList } = this.state;
-
-    const showExportingData = (isExported || isExporting) && (progressList != null);
-
-    return (
-      <Fragment>
-        <h2>{t('Export Archive Data')}</h2>
-
-        <button type="button" className="btn btn-default" disabled={isExporting} onClick={this.openExportModal}>
-          {t('export_management.create_new_archive_data')}
-        </button>
-
-        { showExportingData && (
-          <div className="mt-5">
-            <h3>{t('export_management.exporting_collection_list')}</h3>
-            { this.renderProgressBarsForCollections() }
-            { this.renderProgressBarForZipping() }
-          </div>
-        ) }
-
-        <div className="mt-5">
-          <h3>{t('export_management.exported_data_list')}</h3>
-          <ArchiveFilesTable
-            zipFileStats={this.state.zipFileStats}
-            onZipFileStatRemove={this.onZipFileStatRemove}
-          />
-        </div>
-
-        <SelectCollectionsModal
-          isOpen={this.state.isExportModalOpen}
-          onExportingRequested={this.exportingRequestedHandler}
-          onClose={this.closeExportModal}
-          collections={this.state.collections}
-        />
-      </Fragment>
-    );
-  }
-
-}
-
-ExportArchiveDataPage.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ExportArchiveDataPageWrapper = (props) => {
-  return createSubscribedElement(ExportArchiveDataPage, props, [AppContainer, WebsocketContainer]);
-};
-
-export default withTranslation()(ExportArchiveDataPageWrapper);

+ 0 - 35
src/client/js/components/Admin/FullTextSearchManagement.jsx

@@ -1,35 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
-
-import RebuildIndex from './FullTextSearchManagement/RebuildIndex';
-
-
-class FullTextSearchManagement extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <h2> { t('full_text_search_management.elasticsearch_management') } </h2>
-        <RebuildIndex />
-      </Fragment>
-    );
-  }
-
-}
-
-const FullTextSearchManagementWrapper = (props) => {
-  return createSubscribedElement(FullTextSearchManagement, props, [AppContainer]);
-};
-
-FullTextSearchManagement.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(FullTextSearchManagementWrapper);

+ 0 - 133
src/client/js/components/Admin/FullTextSearchManagement/RebuildIndex.jsx

@@ -1,133 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import WebsocketContainer from '../../../services/WebsocketContainer';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import ProgressBar from '../Common/ProgressBar';
-
-class RebuildIndex extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isProcessing: false,
-      isCompleted: false,
-
-      total: 0,
-      current: 0,
-      skip: 0,
-    };
-
-    this.buildIndex = this.buildIndex.bind(this);
-  }
-
-  componentDidMount() {
-    const socket = this.props.websocketContainer.getWebSocket();
-
-    socket.on('admin:addPageProgress', (data) => {
-      this.setState({
-        isProcessing: true,
-        ...data,
-      });
-    });
-
-    socket.on('admin:finishAddPage', (data) => {
-      this.setState({
-        isProcessing: false,
-        isCompleted: true,
-        ...data,
-      });
-    });
-  }
-
-  async buildIndex() {
-
-    const { appContainer } = this.props;
-    const pageId = this.pageId;
-
-    try {
-      const res = await appContainer.apiPost('/admin/search/build', { page_id: pageId });
-      if (!res.ok) {
-        throw new Error(res.message);
-      }
-
-      this.setState({ isProcessing: true });
-      toastSuccess('Rebuilding is requested');
-    }
-    catch (e) {
-      toastError(e);
-    }
-  }
-
-  renderProgressBar() {
-    const {
-      total, current, skip, isProcessing, isCompleted,
-    } = this.state;
-    const showProgressBar = isProcessing || isCompleted;
-
-    if (!showProgressBar) {
-      return null;
-    }
-
-    const header = isCompleted ? 'Completed' : `Processing.. (${skip} skips)`;
-
-    return (
-      <ProgressBar
-        header={header}
-        currentCount={current}
-        totalCount={total}
-      />
-    );
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <>
-        <div className="row">
-          <div className="col-xs-3 control-label"></div>
-          <div className="col-xs-9">
-            { this.renderProgressBar() }
-
-            <button
-              type="submit"
-              className="btn btn-inverse"
-              onClick={this.buildIndex}
-              disabled={this.state.isProcessing}
-            >
-              { t('full_text_search_management.build_button') }
-            </button>
-
-            <p className="help-block">
-              { t('full_text_search_management.rebuild_description_1') }<br />
-              { t('full_text_search_management.rebuild_description_2') }<br />
-              { t('full_text_search_management.rebuild_description_3') }<br />
-            </p>
-          </div>
-        </div>
-      </>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const RebuildIndexWrapper = (props) => {
-  return createSubscribedElement(RebuildIndex, props, [AppContainer, WebsocketContainer]);
-};
-
-RebuildIndex.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
-};
-
-export default withTranslation()(RebuildIndexWrapper);

+ 181 - 0
src/client/js/components/Admin/Import/GrowiZipImportForm.jsx

@@ -0,0 +1,181 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import * as toastr from 'toastr';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+// import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class GrowiImportForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.initialState = {
+      collections: new Set(),
+      schema: {
+        pages: {},
+        revisions: {},
+        // ...
+      },
+    };
+
+    this.state = this.initialState;
+
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
+    this.import = this.import.bind(this);
+    this.validateForm = this.validateForm.bind(this);
+  }
+
+  toggleCheckbox(e) {
+    const { target } = e;
+    const { name, checked } = target;
+
+    this.setState((prevState) => {
+      const collections = new Set(prevState.collections);
+      if (checked) {
+        collections.add(name);
+      }
+      else {
+        collections.delete(name);
+      }
+      return { collections };
+    });
+  }
+
+  async import(e) {
+    e.preventDefault();
+
+    try {
+      // TODO: use appContainer.apiv3.post
+      const { results } = await this.props.appContainer.apiPost('/v3/import', {
+        fileName: this.props.fileName,
+        collections: Array.from(this.state.collections),
+        schema: this.state.schema,
+      });
+
+      this.setState(this.initialState);
+      this.props.onPostImport();
+
+      // TODO: toastSuccess, toastError
+      toastr.success(undefined, 'Imported documents', {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '1200',
+        extendedTimeOut: '150',
+      });
+
+      for (const { collectionName, failedIds } of results) {
+        if (failedIds.length > 0) {
+          toastr.error(`failed to insert ${failedIds.join(', ')}`, collectionName, {
+            closeButton: true,
+            progressBar: true,
+            newestOnTop: false,
+            timeOut: '30000',
+          });
+        }
+      }
+    }
+    catch (err) {
+      // TODO: toastSuccess, toastError
+      toastr.error(err, 'Error', {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '3000',
+      });
+    }
+  }
+
+  validateForm() {
+    return this.state.collections.size > 0;
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <form className="row" onSubmit={this.import}>
+        <div className="col-xs-12">
+          <table className="table table-bordered table-mapping">
+            <caption>{t('importer_management.growi_settings.uploaded_data')}</caption>
+            <thead>
+              <tr>
+                <th></th>
+                <th>{t('importer_management.growi_settings.extracted_file')}</th>
+                <th>{t('importer_management.growi_settings.collection')}</th>
+              </tr>
+            </thead>
+            <tbody>
+              {this.props.fileStats.map((fileStat) => {
+                  const { fileName, collectionName } = fileStat;
+                  const checked = this.state.collections.has(collectionName);
+                  return (
+                    <Fragment key={collectionName}>
+                      <tr>
+                        <td>
+                          <input
+                            type="checkbox"
+                            id={collectionName}
+                            name={collectionName}
+                            className="form-check-input"
+                            value={collectionName}
+                            checked={checked}
+                            onChange={this.toggleCheckbox}
+                          />
+                        </td>
+                        <td>{fileName}</td>
+                        <td className="text-capitalize">{collectionName}</td>
+                      </tr>
+                      {checked && (
+                        <tr>
+                          <td className="text-muted" colSpan="3">
+                            TBD: define how {collectionName} are imported
+                            {/* TODO: create a component for each collection to modify this.state.schema */}
+                          </td>
+                        </tr>
+                      )}
+                    </Fragment>
+                  );
+                })}
+            </tbody>
+          </table>
+        </div>
+        <div className="col-xs-12 text-center">
+          <button type="submit" className="btn btn-primary mx-1" disabled={!this.validateForm()}>
+            { t('importer_management.import') }
+          </button>
+          <button type="button" className="btn btn-default mx-1" onClick={this.props.onDiscard}>
+            { t('importer_management.growi_settings.discard') }
+          </button>
+        </div>
+      </form>
+    );
+  }
+
+}
+
+GrowiImportForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  fileName: PropTypes.string,
+  fileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
+  onDiscard: PropTypes.func.isRequired,
+  onPostImport: PropTypes.func.isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GrowiImportFormWrapper = (props) => {
+  return createSubscribedElement(GrowiImportForm, props, [AppContainer]);
+};
+
+export default withTranslation()(GrowiImportFormWrapper);

+ 30 - 30
src/client/js/components/Admin/ImportData/GrowiArchiveSection.jsx → src/client/js/components/Admin/Import/GrowiZipImportSection.jsx

@@ -3,21 +3,20 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
+import GrowiZipUploadForm from './GrowiZipUploadForm';
+import GrowiZipImportForm from './GrowiZipImportForm';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
-import UploadForm from './GrowiArchive/UploadForm';
-import ImportForm from './GrowiArchive/ImportForm';
-
-class GrowiArchiveSection extends React.Component {
+class GrowiZipImportSection extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.initialState = {
     this.initialState = {
-      fileName: null,
-      innerFileStats: null,
+      fileName: '',
+      fileStats: [],
     };
     };
 
 
     this.state = this.initialState;
     this.state = this.initialState;
@@ -27,27 +26,17 @@ class GrowiArchiveSection extends React.Component {
     this.resetState = this.resetState.bind(this);
     this.resetState = this.resetState.bind(this);
   }
   }
 
 
-  async componentWillMount() {
-    // get uploaded file status
-    const res = await this.props.appContainer.apiv3Get('/import/status');
-
-    if (res.data.zipFileStat != null) {
-      const { fileName, innerFileStats } = res.data.zipFileStat;
-      this.setState({ fileName, innerFileStats });
-    }
-  }
-
-  handleUpload({ meta, fileName, innerFileStats }) {
+  handleUpload({ meta, fileName, fileStats }) {
     this.setState({
     this.setState({
       fileName,
       fileName,
-      innerFileStats,
+      fileStats,
     });
     });
   }
   }
 
 
   async discardData() {
   async discardData() {
     try {
     try {
       const { fileName } = this.state;
       const { fileName } = this.state;
-      await this.props.appContainer.apiv3Delete('/import/all');
+      await this.props.appContainer.apiDelete(`/v3/import/${this.state.fileName}`, {});
       this.resetState();
       this.resetState();
 
 
       // TODO: toastSuccess, toastError
       // TODO: toastSuccess, toastError
@@ -83,18 +72,29 @@ class GrowiArchiveSection extends React.Component {
 
 
     return (
     return (
       <Fragment>
       <Fragment>
-        <h2>{t('importer_management.import_growi_archive')}</h2>
+        <legend>{t('importer_management.import_form_growi')}</legend>
+
+        <div className="alert alert-warning">
+          <i className="icon-exclamation"></i> { t('importer_management.beta_warning') }
+        </div>
+
+        <div className="well well-sm small">
+          <ul>
+            <li>{t('importer_management.growi_settings.overwrite_documents')}</li>
+          </ul>
+        </div>
 
 
-        { this.state.fileName != null ? (
-          <div className="px-4">
-            <ImportForm
+        {this.state.fileName ? (
+          <Fragment>
+            <GrowiZipImportForm
               fileName={this.state.fileName}
               fileName={this.state.fileName}
-              innerFileStats={this.state.innerFileStats}
+              fileStats={this.state.fileStats}
               onDiscard={this.discardData}
               onDiscard={this.discardData}
+              onPostImport={this.resetState}
             />
             />
-          </div>
+          </Fragment>
         ) : (
         ) : (
-          <UploadForm
+          <GrowiZipUploadForm
             onUpload={this.handleUpload}
             onUpload={this.handleUpload}
           />
           />
         )}
         )}
@@ -104,7 +104,7 @@ class GrowiArchiveSection extends React.Component {
 
 
 }
 }
 
 
-GrowiArchiveSection.propTypes = {
+GrowiZipImportSection.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 };
@@ -112,8 +112,8 @@ GrowiArchiveSection.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const GrowiArchiveSectionWrapper = (props) => {
-  return createSubscribedElement(GrowiArchiveSection, props, [AppContainer]);
+const GrowiZipImportSectionWrapper = (props) => {
+  return createSubscribedElement(GrowiZipImportSection, props, [AppContainer]);
 };
 };
 
 
-export default withTranslation()(GrowiArchiveSectionWrapper);
+export default withTranslation()(GrowiZipImportSectionWrapper);

+ 11 - 11
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx → src/client/js/components/Admin/Import/GrowiZipUploadForm.jsx

@@ -2,11 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { createSubscribedElement } from '../../../UnstatedUtils';
-import AppContainer from '../../../../services/AppContainer';
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
-class UploadForm extends React.Component {
+class GrowiZipUploadForm extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -31,7 +31,8 @@ class UploadForm extends React.Component {
     formData.append('_csrf', this.props.appContainer.csrfToken);
     formData.append('_csrf', this.props.appContainer.csrfToken);
     formData.append('file', this.inputRef.current.files[0]);
     formData.append('file', this.inputRef.current.files[0]);
 
 
-    const { data } = await this.props.appContainer.apiv3Post('/import/upload', formData);
+    // TODO: use appContainer.apiv3.post
+    const { data } = await this.props.appContainer.apiPost('/v3/import/upload', formData);
     this.props.onUpload(data);
     this.props.onUpload(data);
     // TODO: toastSuccess, toastError
     // TODO: toastSuccess, toastError
   }
   }
@@ -50,14 +51,13 @@ class UploadForm extends React.Component {
     return (
     return (
       <form className="form-horizontal" onSubmit={this.uploadZipFile}>
       <form className="form-horizontal" onSubmit={this.uploadZipFile}>
         <fieldset>
         <fieldset>
-          <div className="form-group">
-            <label htmlFor="file" className="col-xs-3 control-label">{t('importer_management.growi_settings.growi_archive_file')}</label>
+          <div className="form-group d-flex align-items-center">
+            <label htmlFor="file" className="col-xs-3 control-label">{t('importer_management.growi_settings.zip_file')}</label>
             <div className="col-xs-6">
             <div className="col-xs-6">
               <input
               <input
                 type="file"
                 type="file"
                 name="file"
                 name="file"
                 className="form-control-file"
                 className="form-control-file"
-                accept=".zip"
                 ref={this.inputRef}
                 ref={this.inputRef}
                 onChange={this.changeFileName}
                 onChange={this.changeFileName}
               />
               />
@@ -77,7 +77,7 @@ class UploadForm extends React.Component {
 
 
 }
 }
 
 
-UploadForm.propTypes = {
+GrowiZipUploadForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   onUpload: PropTypes.func.isRequired,
   onUpload: PropTypes.func.isRequired,
@@ -86,8 +86,8 @@ UploadForm.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const UploadFormWrapper = (props) => {
-  return createSubscribedElement(UploadForm, props, [AppContainer]);
+const GrowiZipUploadFormWrapper = (props) => {
+  return createSubscribedElement(GrowiZipUploadForm, props, [AppContainer]);
 };
 };
 
 
-export default withTranslation()(UploadFormWrapper);
+export default withTranslation()(GrowiZipUploadFormWrapper);

+ 0 - 52
src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx

@@ -1,52 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
-
-import { createSubscribedElement } from '../../../UnstatedUtils';
-
-
-class ErrorViewer extends React.Component {
-
-  render() {
-    const { errors } = this.props;
-
-    let value = '(no errors)';
-    if (errors != null && errors.length > 0) {
-      const lines = errors.map((obj) => {
-        return JSON.stringify(obj);
-      });
-      value = lines.join('\n');
-    }
-
-    return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header closeButton className="bg-danger">
-          <Modal.Title className="text-white">Errors</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          <textarea className="form-control" rows="8" readOnly wrap="off" defaultValue={value}></textarea>
-        </Modal.Body>
-      </Modal>
-    );
-  }
-
-}
-
-ErrorViewer.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-
-  errors: PropTypes.arrayOf(PropTypes.object),
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ErrorViewerWrapper = (props) => {
-  return createSubscribedElement(ErrorViewer, props, []);
-};
-
-export default withTranslation()(ErrorViewerWrapper);

+ 0 - 228
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -1,228 +0,0 @@
-/* eslint-disable react/no-danger */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
-
-import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
-
-import { createSubscribedElement } from '../../../UnstatedUtils';
-import AppContainer from '../../../../services/AppContainer';
-// import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-
-class ImportCollectionConfigurationModal extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      option: null,
-    };
-
-    this.initialize = this.initialize.bind(this);
-    this.updateOption = this.updateOption.bind(this);
-  }
-
-  async initialize() {
-    await this.setState({
-      option: Object.assign({}, this.props.option), // clone
-    });
-  }
-
-  /**
-   * invoked when the value of control is changed
-   * @param {object} updateObj
-   */
-  changeHandler(updateObj) {
-    const { option } = this.state;
-    const newOption = Object.assign(option, updateObj);
-    this.setState({ option: newOption });
-  }
-
-  updateOption() {
-    const {
-      collectionName, onOptionChange, onClose,
-    } = this.props;
-
-    if (onOptionChange != null) {
-      onOptionChange(collectionName, this.state.option);
-    }
-
-    onClose();
-  }
-
-  renderPagesContents() {
-    const { t } = this.props;
-    const { option } = this.state;
-
-    const translationBase = 'importer_management.growi_settings.configuration.pages';
-
-    /* eslint-disable react/no-unescaped-entities */
-    return (
-      <>
-        <div className="checkbox checkbox-warning">
-          <input
-            id="cbOpt4"
-            type="checkbox"
-            checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
-          />
-          <label htmlFor="cbOpt4">
-            {t(`${translationBase}.overwrite_author.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
-          </label>
-        </div>
-        <div className="checkbox checkbox-warning">
-          <input
-            id="cbOpt1"
-            type="checkbox"
-            checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })}
-          />
-          <label htmlFor="cbOpt1">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Anyone with the link') })}
-            <p
-              className="help-block mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Anyone with the link') }) }}
-            />
-          </label>
-        </div>
-        <div className="checkbox checkbox-warning">
-          <input
-            id="cbOpt2"
-            type="checkbox"
-            checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
-          />
-          <label htmlFor="cbOpt2">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Just me') })}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Just me') }) }} />
-          </label>
-        </div>
-        <div className="checkbox checkbox-warning">
-          <input
-            id="cbOpt3"
-            type="checkbox"
-            checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })}
-          />
-          <label htmlFor="cbOpt3">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only inside the group') })}
-            <p
-              className="help-block mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only inside the group') }) }}
-            />
-          </label>
-        </div>
-        <div className="checkbox checkbox-default">
-          <input
-            id="cbOpt5"
-            type="checkbox"
-            checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
-          />
-          <label htmlFor="cbOpt5">
-            {t(`${translationBase}.initialize_meta_datas.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
-          </label>
-        </div>
-        <div className="checkbox checkbox-default">
-          <input
-            id="cbOpt6"
-            type="checkbox"
-            checked={option.initHackmdDatas || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ initHackmdDatas: !option.initHackmdDatas })}
-          />
-          <label htmlFor="cbOpt6">
-            {t(`${translationBase}.initialize_hackmd_related_datas.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_hackmd_related_datas.desc`) }} />
-          </label>
-        </div>
-      </>
-    );
-    /* eslint-enable react/no-unescaped-entities */
-  }
-
-  renderRevisionsContents() {
-    const { t } = this.props;
-    const { option } = this.state;
-
-    const translationBase = 'importer_management.growi_settings.configuration.revisions';
-
-    /* eslint-disable react/no-unescaped-entities */
-    return (
-      <>
-        <div className="checkbox checkbox-warning">
-          <input
-            id="cbOpt1"
-            type="checkbox"
-            checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
-          />
-          <label htmlFor="cbOpt1">
-            {t(`${translationBase}.overwrite_author.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
-          </label>
-        </div>
-      </>
-    );
-    /* eslint-enable react/no-unescaped-entities */
-  }
-
-  render() {
-    const { t, collectionName } = this.props;
-    const { option } = this.state;
-
-    let contents = null;
-    if (option != null) {
-      switch (collectionName) {
-        case 'pages':
-          contents = this.renderPagesContents();
-          break;
-        case 'revisions':
-          contents = this.renderRevisionsContents();
-          break;
-      }
-    }
-
-    return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose} onEnter={this.initialize}>
-        <Modal.Header closeButton>
-          <Modal.Title>{`'${collectionName}'`} Configuration</Modal.Title>
-        </Modal.Header>
-
-        <Modal.Body>
-          {contents}
-        </Modal.Body>
-
-        <Modal.Footer>
-          <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('Cancel')}</button>
-          <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
-        </Modal.Footer>
-      </Modal>
-    );
-  }
-
-}
-
-ImportCollectionConfigurationModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-  onOptionChange: PropTypes.func,
-
-  collectionName: PropTypes.string,
-  option: PropTypes.instanceOf(GrowiArchiveImportOption).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ImportCollectionConfigurationModalWrapper = (props) => {
-  return createSubscribedElement(ImportCollectionConfigurationModal, props, [AppContainer]);
-};
-
-export default withTranslation()(ImportCollectionConfigurationModalWrapper);

+ 0 - 253
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -1,253 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-// eslint-disable-next-line no-unused-vars
-import { withTranslation } from 'react-i18next';
-
-import ProgressBar from 'react-bootstrap/es/ProgressBar';
-
-import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
-
-
-const MODE_ATTR_MAP = {
-  insert: { color: 'info', icon: 'icon-plus', label: 'Insert' },
-  upsert: { color: 'success', icon: 'icon-plus', label: 'Upsert' },
-  flushAndInsert: { color: 'danger', icon: 'icon-refresh', label: 'Flush and Insert' },
-};
-
-export const DEFAULT_MODE = 'insert';
-
-export const MODE_RESTRICTED_COLLECTION = {
-  configs: ['flushAndInsert'],
-  users: ['insert', 'upsert'],
-};
-
-export default class ImportCollectionItem extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.changeHandler = this.changeHandler.bind(this);
-    this.modeSelectedHandler = this.modeSelectedHandler.bind(this);
-    this.configButtonClickedHandler = this.configButtonClickedHandler.bind(this);
-    this.errorLinkClickedHandler = this.errorLinkClickedHandler.bind(this);
-  }
-
-  changeHandler(e) {
-    const { collectionName, onChange } = this.props;
-
-    if (onChange != null) {
-      onChange(collectionName, e.target.checked);
-    }
-  }
-
-  modeSelectedHandler(mode) {
-    const { collectionName, onOptionChange } = this.props;
-
-    if (onOptionChange == null) {
-      return;
-    }
-
-    onOptionChange(collectionName, { mode });
-  }
-
-  configButtonClickedHandler() {
-    const { collectionName, onConfigButtonClicked } = this.props;
-
-    if (onConfigButtonClicked == null) {
-      return;
-    }
-
-    onConfigButtonClicked(collectionName);
-  }
-
-  errorLinkClickedHandler() {
-    const { collectionName, onErrorLinkClicked } = this.props;
-
-    if (onErrorLinkClicked == null) {
-      return;
-    }
-
-    onErrorLinkClicked(collectionName);
-  }
-
-  renderModeLabel(mode, isColorized = false) {
-    const attrMap = MODE_ATTR_MAP[mode];
-    const className = isColorized ? `text-${attrMap.color}` : '';
-    return <span className={className}><i className={attrMap.icon}></i> {attrMap.label}</span>;
-  }
-
-  renderCheckbox() {
-    const {
-      collectionName, isSelected, isImporting,
-    } = this.props;
-
-    return (
-      <div className="checkbox checkbox-info my-0">
-        <input
-          type="checkbox"
-          id={collectionName}
-          name={collectionName}
-          className="form-check-input"
-          value={collectionName}
-          checked={isSelected}
-          disabled={isImporting}
-          onChange={this.changeHandler}
-        />
-        <label className="text-capitalize form-check-label" htmlFor={collectionName}>
-          {collectionName}
-        </label>
-      </div>
-    );
-  }
-
-  renderModeSelector() {
-    const {
-      collectionName, option, isImporting,
-    } = this.props;
-
-    const attrMap = MODE_ATTR_MAP[option.mode];
-    const btnColor = `btn-${attrMap.color}`;
-
-    const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
-
-    return (
-      <span className="d-inline-flex align-items-center">
-        Mode:&nbsp;
-        <div className="dropdown d-inline-block">
-          <button
-            className={`btn ${btnColor} btn-xs dropdown-toggle`}
-            type="button"
-            id="ddmMode"
-            disabled={isImporting}
-            data-toggle="dropdown"
-            aria-haspopup="true"
-            aria-expanded="true"
-          >
-            {this.renderModeLabel(option.mode)}
-            <span className="caret ml-2"></span>
-          </button>
-          <ul className="dropdown-menu" aria-labelledby="ddmMode">
-            { modes.map((mode) => {
-              return (
-                <li key={`buttonMode_${mode}`}>
-                  <a type="button" role="button" onClick={() => this.modeSelectedHandler(mode)}>
-                    {this.renderModeLabel(mode, true)}
-                  </a>
-                </li>
-              );
-            }) }
-          </ul>
-        </div>
-      </span>
-    );
-  }
-
-  renderConfigButton() {
-    const { isImporting, isConfigButtonAvailable } = this.props;
-
-    return (
-      <button
-        type="button"
-        className="btn btn-default btn-xs ml-2"
-        disabled={isImporting || !isConfigButtonAvailable}
-        onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
-      >
-        <i className="icon-settings"></i>
-      </button>
-    );
-  }
-
-  renderProgressBar() {
-    const {
-      isImporting, insertedCount, modifiedCount, errorsCount,
-    } = this.props;
-
-    const total = insertedCount + modifiedCount + errorsCount;
-
-    return (
-      <ProgressBar className="mb-0">
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={insertedCount} bsStyle="info" />
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={modifiedCount} bsStyle="success" />
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={errorsCount} bsStyle="danger" />
-      </ProgressBar>
-    );
-  }
-
-  renderBody() {
-    const { isImporting, isImported } = this.props;
-
-    if (!isImporting && !isImported) {
-      return 'Ready';
-    }
-
-    const { insertedCount, modifiedCount, errorsCount } = this.props;
-    return (
-      <div className="w-100 text-center">
-        <span className="text-info"><strong>{insertedCount}</strong> Inserted</span>,&nbsp;
-        <span className="text-success"><strong>{modifiedCount}</strong> Modified</span>,&nbsp;
-        { errorsCount > 0
-          ? <a className="text-danger" role="button" onClick={this.errorLinkClickedHandler}><u><strong>{errorsCount}</strong> Failed</u></a>
-          : <span className="text-muted"><strong>0</strong> Failed</span>
-        }
-      </div>
-    );
-
-  }
-
-  render() {
-    const {
-      isSelected,
-    } = this.props;
-
-    return (
-      <div className="panel panel-default">
-        <div className="panel-heading">
-          <div className="d-flex justify-content-between align-items-center">
-            {/* left */}
-            {this.renderCheckbox()}
-            {/* right */}
-            <span className="d-flex align-items-center">
-              {this.renderModeSelector()}
-              {this.renderConfigButton()}
-            </span>
-          </div>
-        </div>
-        { isSelected && (
-          <>
-            {this.renderProgressBar()}
-            <div className="panel-body">
-              {this.renderBody()}
-            </div>
-          </>
-        ) }
-      </div>
-    );
-  }
-
-}
-
-ImportCollectionItem.propTypes = {
-  collectionName: PropTypes.string.isRequired,
-  isSelected: PropTypes.bool.isRequired,
-  option: PropTypes.instanceOf(GrowiArchiveImportOption).isRequired,
-
-  isImporting: PropTypes.bool.isRequired,
-  isImported: PropTypes.bool.isRequired,
-  insertedCount: PropTypes.number,
-  modifiedCount: PropTypes.number,
-  errorsCount: PropTypes.number,
-
-  isConfigButtonAvailable: PropTypes.bool,
-
-  onChange: PropTypes.func,
-  onOptionChange: PropTypes.func,
-  onConfigButtonClicked: PropTypes.func,
-  onErrorLinkClicked: PropTypes.func,
-};
-
-ImportCollectionItem.defaultProps = {
-  insertedCount: 0,
-  modifiedCount: 0,
-  errorsCount: 0,
-};

+ 0 - 507
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -1,507 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
-import ImportOptionForPages from '@commons/models/admin/import-option-for-pages';
-import ImportOptionForRevisions from '@commons/models/admin/import-option-for-revisions';
-
-import { createSubscribedElement } from '../../../UnstatedUtils';
-import AppContainer from '../../../../services/AppContainer';
-import WebsocketContainer from '../../../../services/WebsocketContainer';
-import { toastSuccess, toastError } from '../../../../util/apiNotification';
-
-
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
-import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
-import ErrorViewer from './ErrorViewer';
-
-
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
-const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
-];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
-
-const IMPORT_OPTION_CLASS_MAPPING = {
-  pages: ImportOptionForPages,
-  revisions: ImportOptionForRevisions,
-};
-
-class ImportForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.initialState = {
-      isImporting: false,
-      isImported: false,
-      progressMap: [],
-      errorsMap: [],
-
-      selectedCollections: new Set(),
-
-      // store relations from collection name to file name
-      collectionNameToFileNameMap: {},
-      // store relations from collection name to GrowiArchiveImportOption instance
-      optionsMap: {},
-
-      isConfigurationModalOpen: false,
-      collectionNameForConfiguration: null,
-
-      isErrorsViewerOpen: false,
-      collectionNameForErrorsViewer: null,
-
-      canImport: false,
-      warnForPageGroups: [],
-      warnForUserGroups: [],
-      warnForConfigGroups: [],
-      warnForOtherGroups: [],
-    };
-
-    this.props.innerFileStats.forEach((fileStat) => {
-      const { fileName, collectionName } = fileStat;
-      this.initialState.collectionNameToFileNameMap[collectionName] = fileName;
-
-      // determine initial mode
-      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
-        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
-        : DEFAULT_MODE;
-      // create GrowiArchiveImportOption instance
-      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      this.initialState.optionsMap[collectionName] = new ImportOption(initialMode);
-    });
-
-    this.state = this.initialState;
-
-    this.toggleCheckbox = this.toggleCheckbox.bind(this);
-    this.checkAll = this.checkAll.bind(this);
-    this.uncheckAll = this.uncheckAll.bind(this);
-    this.updateOption = this.updateOption.bind(this);
-    this.openConfigurationModal = this.openConfigurationModal.bind(this);
-    this.showErrorsViewer = this.showErrorsViewer.bind(this);
-    this.validate = this.validate.bind(this);
-    this.import = this.import.bind(this);
-  }
-
-  get allCollectionNames() {
-    return Object.keys(this.state.collectionNameToFileNameMap);
-  }
-
-  componentWillMount() {
-    this.setupWebsocketEventHandler();
-  }
-
-  componentWillUnmount() {
-    this.teardownWebsocketEventHandler();
-  }
-
-  setupWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
-
-    // websocket event
-    // eslint-disable-next-line object-curly-newline
-    socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
-      const { progressMap, errorsMap } = this.state;
-      progressMap[collectionName] = collectionProgress;
-
-      const errors = errorsMap[collectionName] || [];
-      errorsMap[collectionName] = errors.concat(appendedErrors);
-
-      this.setState({
-        isImporting: true,
-        progressMap,
-        errorsMap,
-      });
-    });
-
-    // websocket event
-    socket.on('admin:onTerminateForImport', () => {
-      this.setState({
-        isImporting: false,
-        isImported: true,
-      });
-
-      toastSuccess(undefined, 'Import process has terminated.');
-    });
-
-    // websocket event
-    socket.on('admin:onErrorForImport', (err) => {
-      this.setState({
-        isImporting: false,
-        isImported: false,
-      });
-
-      toastError(err, 'Import process has failed.');
-    });
-  }
-
-  teardownWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
-
-    socket.removeAllListeners('admin:onProgressForImport');
-    socket.removeAllListeners('admin:onTerminateForImport');
-  }
-
-  async toggleCheckbox(collectionName, bool) {
-    const selectedCollections = new Set(this.state.selectedCollections);
-    if (bool) {
-      selectedCollections.add(collectionName);
-    }
-    else {
-      selectedCollections.delete(collectionName);
-    }
-
-    await this.setState({ selectedCollections });
-
-    this.validate();
-  }
-
-  async checkAll() {
-    await this.setState({ selectedCollections: new Set(this.allCollectionNames) });
-    this.validate();
-  }
-
-  async uncheckAll() {
-    await this.setState({ selectedCollections: new Set() });
-    this.validate();
-  }
-
-  updateOption(collectionName, data) {
-    const { optionsMap } = this.state;
-    const options = optionsMap[collectionName];
-
-    // merge
-    Object.assign(options, data);
-
-    optionsMap[collectionName] = options;
-    this.setState({ optionsMap });
-  }
-
-  openConfigurationModal(collectionName) {
-    this.setState({ isConfigurationModalOpen: true, collectionNameForConfiguration: collectionName });
-  }
-
-  showErrorsViewer(collectionName) {
-    this.setState({ isErrorsViewerOpen: true, collectionNameForErrorsViewer: collectionName });
-  }
-
-  async validate() {
-    // init errors
-    await this.setState({
-      warnForPageGroups: [],
-      warnForUserGroups: [],
-      warnForConfigGroups: [],
-      warnForOtherGroups: [],
-    });
-
-    await this.validateCollectionSize();
-    await this.validatePagesCollectionPairs();
-    await this.validateExternalAccounts();
-    await this.validateUserGroups();
-    await this.validateUserGroupRelations();
-
-    const errors = [
-      ...this.state.warnForPageGroups,
-      ...this.state.warnForUserGroups,
-      ...this.state.warnForConfigGroups,
-      ...this.state.warnForOtherGroups,
-    ];
-    const canImport = errors.length === 0;
-
-    this.setState({ canImport });
-  }
-
-  async validateCollectionSize(validationErrors) {
-    const { t } = this.props;
-    const { warnForOtherGroups, selectedCollections } = this.state;
-
-    if (selectedCollections.size === 0) {
-      warnForOtherGroups.push(t('importer_management.growi_settings.errors.at_least_one'));
-    }
-
-    this.setState({ warnForOtherGroups });
-  }
-
-  async validatePagesCollectionPairs() {
-    const { t } = this.props;
-    const { warnForPageGroups, selectedCollections } = this.state;
-
-    const pageRelatedCollectionsLength = ['pages', 'revisions'].filter((collectionName) => {
-      return selectedCollections.has(collectionName);
-    }).length;
-
-    // MUST be included both or neither when importing
-    if (pageRelatedCollectionsLength !== 0 && pageRelatedCollectionsLength !== 2) {
-      warnForPageGroups.push(t('importer_management.growi_settings.errors.page_and_revision'));
-    }
-
-    this.setState({ warnForPageGroups });
-  }
-
-  async validateExternalAccounts() {
-    const { t } = this.props;
-    const { warnForUserGroups, selectedCollections } = this.state;
-
-    // MUST include also 'users' if 'externalaccounts' is selected
-    if (selectedCollections.has('externalaccounts')) {
-      if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' }));
-      }
-    }
-
-    this.setState({ warnForUserGroups });
-  }
-
-  async validateUserGroups() {
-    const { t } = this.props;
-    const { warnForUserGroups, selectedCollections } = this.state;
-
-    // MUST include also 'users' if 'usergroups' is selected
-    if (selectedCollections.has('usergroups')) {
-      if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' }));
-      }
-    }
-
-    this.setState({ warnForUserGroups });
-  }
-
-  async validateUserGroupRelations() {
-    const { t } = this.props;
-    const { warnForUserGroups, selectedCollections } = this.state;
-
-    // MUST include also 'usergroups' if 'usergrouprelations' is selected
-    if (selectedCollections.has('usergrouprelations')) {
-      if (!selectedCollections.has('usergroups')) {
-        warnForUserGroups.push(t('importer_management.growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' }));
-      }
-    }
-
-    this.setState({ warnForUserGroups });
-  }
-
-  async import() {
-    const { appContainer, fileName, onPostImport } = this.props;
-    const { selectedCollections, optionsMap } = this.state;
-
-    // init progress data
-    await this.setState({
-      isImporting: true,
-      progressMap: [],
-      errorsMap: [],
-    });
-
-    try {
-      // TODO: use appContainer.apiv3.post
-      await appContainer.apiv3Post('/import', {
-        fileName,
-        collections: Array.from(selectedCollections),
-        optionsMap,
-      });
-
-      if (onPostImport != null) {
-        onPostImport();
-      }
-
-      toastSuccess(undefined, 'Import process has requested.');
-    }
-    catch (err) {
-      toastError(err, 'Import request failed.');
-    }
-  }
-
-  renderWarnForGroups(errors, key) {
-    if (errors.length === 0) {
-      return null;
-    }
-
-    return (
-      <div key={key} className="alert alert-warning">
-        <ul>
-          { errors.map((error, index) => {
-            // eslint-disable-next-line react/no-array-index-key
-            return <li key={`${key}-${index}`}>{error}</li>;
-          }) }
-        </ul>
-      </div>
-    );
-  }
-
-  renderGroups(groupList, groupName, errors, { wellContent } = {}) {
-    const collectionNames = groupList.filter((collectionName) => {
-      return this.allCollectionNames.includes(collectionName);
-    });
-
-    if (collectionNames.length === 0) {
-      return null;
-    }
-
-    return (
-      <div className="mt-4">
-        <legend>{groupName} Collections</legend>
-        { wellContent != null && (
-          <div className="well well-sm small">
-            <ul>
-              <li>{wellContent}</li>
-            </ul>
-          </div>
-        ) }
-        { this.renderImportItems(collectionNames) }
-        { this.renderWarnForGroups(errors, `warnFor${groupName}`) }
-      </div>
-    );
-  }
-
-  renderOthers() {
-    const collectionNames = this.allCollectionNames.filter((collectionName) => {
-      return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
-    });
-
-    return this.renderGroups(collectionNames, 'Other', this.state.warnForOtherGroups);
-  }
-
-  renderImportItems(collectionNames) {
-    const {
-      isImporting,
-      isImported,
-      progressMap,
-      errorsMap,
-
-      selectedCollections,
-      optionsMap,
-    } = this.state;
-
-    return (
-      <div className="row">
-        {collectionNames.map((collectionName) => {
-          const collectionProgress = progressMap[collectionName];
-          const errors = errorsMap[collectionName];
-          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
-
-          return (
-            <div className="col-xs-6 my-1" key={collectionName}>
-              <ImportCollectionItem
-                isImporting={isImporting}
-                isImported={collectionProgress ? isImported : false}
-                insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
-                modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
-                errorsCount={errors ? errors.length : 0}
-
-                collectionName={collectionName}
-                isSelected={selectedCollections.has(collectionName)}
-                option={optionsMap[collectionName]}
-
-                isConfigButtonAvailable={isConfigButtonAvailable}
-
-                onChange={this.toggleCheckbox}
-                onOptionChange={this.updateOption}
-                onConfigButtonClicked={this.openConfigurationModal}
-                onErrorLinkClicked={this.showErrorsViewer}
-              />
-            </div>
-          );
-        })}
-      </div>
-    );
-  }
-
-  renderConfigurationModal() {
-    const { isConfigurationModalOpen, collectionNameForConfiguration: collectionName, optionsMap } = this.state;
-
-    if (collectionName == null) {
-      return null;
-    }
-
-    return (
-      <ImportCollectionConfigurationModal
-        isOpen={isConfigurationModalOpen}
-        onClose={() => this.setState({ isConfigurationModalOpen: false })}
-        onOptionChange={this.updateOption}
-        collectionName={collectionName}
-        option={optionsMap[collectionName]}
-      />
-    );
-  }
-
-  renderErrorsViewer() {
-    const { isErrorsViewerOpen, errorsMap, collectionNameForErrorsViewer } = this.state;
-    const errors = errorsMap[collectionNameForErrorsViewer];
-
-    return (
-      <ErrorViewer
-        isOpen={isErrorsViewerOpen}
-        onClose={() => this.setState({ isErrorsViewerOpen: false })}
-        errors={errors}
-      />
-    );
-  }
-
-  render() {
-    const { t } = this.props;
-    const {
-      canImport, isImporting,
-      warnForPageGroups, warnForUserGroups, warnForConfigGroups,
-    } = this.state;
-
-    return (
-      <>
-        <form className="form-inline">
-          <div className="form-group">
-            <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
-              <i className="fa fa-check-square-o"></i> {t('export_management.check_all')}
-            </button>
-          </div>
-          <div className="form-group">
-            <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
-              <i className="fa fa-square-o"></i> {t('export_management.uncheck_all')}
-            </button>
-          </div>
-        </form>
-
-        { this.renderGroups(GROUPS_PAGE, 'Page', warnForPageGroups, { wellContent: t('importer_management.growi_settings.overwrite_documents') }) }
-        { this.renderGroups(GROUPS_USER, 'User', warnForUserGroups) }
-        { this.renderGroups(GROUPS_CONFIG, 'Config', warnForConfigGroups) }
-        { this.renderOthers() }
-
-        <div className="mt-4 text-center">
-          <button type="button" className="btn btn-default mx-1" onClick={this.props.onDiscard}>
-            { t('importer_management.growi_settings.discard') }
-          </button>
-          <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
-            { t('importer_management.import') }
-          </button>
-        </div>
-
-        { this.renderConfigurationModal() }
-        { this.renderErrorsViewer() }
-      </>
-    );
-  }
-
-}
-
-ImportForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
-
-  fileName: PropTypes.string,
-  innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
-  onDiscard: PropTypes.func.isRequired,
-  onPostImport: PropTypes.func,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ImportFormWrapper = (props) => {
-  return createSubscribedElement(ImportForm, props, [AppContainer, WebsocketContainer]);
-};
-
-export default withTranslation()(ImportFormWrapper);

+ 0 - 347
src/client/js/components/Admin/ImportDataPage.jsx

@@ -1,347 +0,0 @@
-import React, { Fragment } from 'react';
-import { withTranslation } from 'react-i18next';
-import PropTypes from 'prop-types';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../UnstatedUtils';
-import { toastSuccess, toastError } from '../../util/apiNotification';
-
-import AppContainer from '../../services/AppContainer';
-
-import GrowiArchiveSection from './ImportData/GrowiArchiveSection';
-
-const logger = loggerFactory('growi:importer');
-
-class ImportDataPage extends React.Component {
-
-  constructor(props) {
-    super(props);
-    this.state = {
-      esaTeamName: '',
-      esaAccessToken: '',
-      qiitaTeamName: '',
-      qiitaAccessToken: '',
-    };
-
-    this.esaHandleSubmit = this.esaHandleSubmit.bind(this);
-    this.esaHandleSubmitTest = this.esaHandleSubmitTest.bind(this);
-    this.esaHandleSubmitUpdate = this.esaHandleSubmitUpdate.bind(this);
-    this.qiitaHandleSubmit = this.qiitaHandleSubmit.bind(this);
-    this.qiitaHandleSubmitTest = this.qiitaHandleSubmitTest.bind(this);
-    this.qiitaHandleSubmitUpdate = this.qiitaHandleSubmitUpdate.bind(this);
-    this.handleInputValue = this.handleInputValue.bind(this);
-  }
-
-  handleInputValue(event) {
-    this.setState({
-      [event.target.name]: event.target.value,
-    });
-  }
-
-  async esaHandleSubmit() {
-    try {
-      const params = {
-        'importer:esa:team_name': this.state.esaTeamName,
-        'importer:esa:access_token': this.state.esaAccessToken,
-      };
-      await this.props.appContainer.apiPost('/admin/import/esa', params);
-      toastSuccess('Import posts from esa success.');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err, 'Error occurred in importing pages from esa.io');
-    }
-  }
-
-  async esaHandleSubmitTest() {
-    try {
-      const params = {
-        'importer:esa:team_name': this.state.esaTeamName,
-        'importer:esa:access_token': this.state.esaAccessToken,
-      };
-      await this.props.appContainer.apiPost('/admin/import/testEsaAPI', params);
-      toastSuccess('Test connection to esa success.');
-    }
-    catch (error) {
-      toastError(error, 'Test connection to esa failed.');
-    }
-  }
-
-  async esaHandleSubmitUpdate() {
-    const params = {
-      'importer:esa:team_name': this.state.esaTeamName,
-      'importer:esa:access_token': this.state.esaAccessToken,
-    };
-    try {
-      await this.props.appContainer.apiPost('/admin/settings/importerEsa', params);
-      toastSuccess('Updated');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err, 'Errors');
-    }
-  }
-
-  async qiitaHandleSubmit() {
-    try {
-      const params = {
-        'importer:qiita:team_name': this.state.qiitaTeamName,
-        'importer:qiita:access_token': this.state.qiitaAccessToken,
-      };
-      await this.props.appContainer.apiPost('/admin/import/qiita', params);
-      toastSuccess('Import posts from qiita:team success.');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err, 'Error occurred in importing pages from qiita:team');
-    }
-  }
-
-
-  async qiitaHandleSubmitTest() {
-    try {
-      const params = {
-        'importer:qiita:team_name': this.state.qiitaTeamName,
-        'importer:qiita:access_token': this.state.qiitaAccessToken,
-      };
-      await this.props.appContainer.apiPost('/admin/import/testQiitaAPI', params);
-      toastSuccess('Test connection to qiita:team success.');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err, 'Test connection to qiita:team failed.');
-    }
-  }
-
-  async qiitaHandleSubmitUpdate() {
-    const params = {
-      'importer:qiita:team_name': this.state.qiitaTeamName,
-      'importer:qiita:access_token': this.state.qiitaAccessToken,
-    };
-    try {
-      await this.props.appContainer.apiPost('/admin/settings/importerQiita', params);
-      toastSuccess('Updated');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err, 'Errors');
-    }
-  }
-
-  render() {
-    const {
-      esaTeamName, esaAccessToken, qiitaTeamName, qiitaAccessToken,
-    } = this.state;
-    const { t } = this.props;
-    return (
-      <Fragment>
-        <GrowiArchiveSection />
-
-        <form
-          className="form-horizontal mt-5"
-          id="importerSettingFormEsa"
-          role="form"
-        >
-          <fieldset>
-            <legend>{ t('importer_management.import_from', { from: 'esa.io' }) }</legend>
-            <table className="table table-bordered table-mapping">
-              <thead>
-                <tr>
-                  <th width="45%">esa.io</th>
-                  <th width="10%"></th>
-                  <th>GROWI</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr>
-                  <th>{ t('Article') }</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
-                  <th>{ t('Page') }</th>
-                </tr>
-                <tr>
-                  <th>{ t('Category') }</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
-                  <th>{ t('Page Path') }</th>
-                </tr>
-                <tr>
-                  <th>{ t('User') }</th>
-                  <th></th>
-                  <th>(TBD)</th>
-                </tr>
-              </tbody>
-            </table>
-
-            <div className="well well-sm mb-0 small">
-              <ul>
-                <li>{ t('importer_management.page_skip') }</li>
-              </ul>
-            </div>
-
-            <div className="form-group">
-              <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
-            </div>
-
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:esa:team_name]" className="col-xs-3 control-label">
-                { t('importer_management.esa_settings.team_name') }
-              </label>
-              <div className="col-xs-6">
-                <input className="form-control" type="text" name="esaTeamName" value={esaTeamName} onChange={this.handleInputValue} />
-              </div>
-
-            </div>
-
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:esa:access_token]" className="col-xs-3 control-label">
-                { t('importer_management.esa_settings.access_token') }
-              </label>
-              <div className="col-xs-6">
-                <input className="form-control" type="password" name="esaAccessToken" value={esaAccessToken} onChange={this.handleInputValue} />
-              </div>
-            </div>
-
-            <div className="form-group">
-              <div className="col-xs-offset-3 col-xs-6">
-                <input
-                  id="testConnectionToEsa"
-                  type="button"
-                  className="btn btn-primary btn-esa"
-                  name="Esa"
-                  onClick={this.esaHandleSubmit}
-                  value={t('importer_management.import')}
-                />
-                <input type="button" className="btn btn-secondary" onClick={this.esaHandleSubmitUpdate} value={t('Update')} />
-                <span className="col-xs-offset-1">
-                  <input
-                    name="Esa"
-                    type="button"
-                    id="importFromEsa"
-                    className="btn btn-default btn-esa"
-                    onClick={this.esaHandleSubmitTest}
-                    value={t('importer_management.esa_settings.test_connection')}
-                  />
-                </span>
-
-              </div>
-            </div>
-          </fieldset>
-        </form>
-
-        <form
-          className="form-horizontal mt-5"
-          id="importerSettingFormQiita"
-          role="form"
-        >
-          <fieldset>
-            <legend>{ t('importer_management.import_from', { from: 'Qiita:Team' }) }</legend>
-            <table className="table table-bordered table-mapping">
-              <thead>
-                <tr>
-                  <th width="45%">Qiita:Team</th>
-                  <th width="10%"></th>
-                  <th>GROWI</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr>
-                  <th>{ t('Article') }</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
-                  <th>{ t('Page') }</th>
-                </tr>
-                <tr>
-                  <th>{ t('Tag')}</th>
-                  <th></th>
-                  <th>-</th>
-                </tr>
-                <tr>
-                  <th>{ t('importer_management.Directory_hierarchy_tag') }</th>
-                  <th></th>
-                  <th>(TBD)</th>
-                </tr>
-                <tr>
-                  <th>{ t('User') }</th>
-                  <th></th>
-                  <th>(TBD)</th>
-                </tr>
-              </tbody>
-            </table>
-            <div className="well well-sm mb-0 small">
-              <ul>
-                <li>{ t('importer_management.page_skip') }</li>
-              </ul>
-            </div>
-
-            <div className="form-group">
-              <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
-            </div>
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:qiita:team_name]" className="col-xs-3 control-label">
-                { t('importer_management.qiita_settings.team_name') }
-              </label>
-              <div className="col-xs-6">
-                <input className="form-control" type="text" name="qiitaTeamName" value={qiitaTeamName} onChange={this.handleInputValue} />
-              </div>
-            </div>
-
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:qiita:access_token]" className="col-xs-3 control-label">
-                { t('importer_management.qiita_settings.access_token') }
-              </label>
-              <div className="col-xs-6">
-                <input className="form-control" type="password" name="qiitaAccessToken" value={qiitaAccessToken} onChange={this.handleInputValue} />
-              </div>
-            </div>
-
-
-            <div className="form-group">
-              <div className="col-xs-offset-3 col-xs-6">
-                <input
-                  id="testConnectionToQiita"
-                  type="button"
-                  className="btn btn-primary btn-qiita"
-                  name="Qiita"
-                  onClick={this.qiitaHandleSubmit}
-                  value={t('importer_management.import')}
-                />
-                <input type="button" className="btn btn-secondary" onClick={this.qiitaHandleSubmitUpdate} value={t('Update')} />
-                <span className="col-xs-offset-1">
-                  <input
-                    name="Qiita"
-                    type="button"
-                    id="importFromQiita"
-                    className="btn btn-default btn-qiita"
-                    onClick={this.qiitaHandleSubmitTest}
-                    value={t('importer_management.qiita_settings.test_connection')}
-                  />
-                </span>
-
-              </div>
-            </div>
-
-
-          </fieldset>
-
-
-        </form>
-      </Fragment>
-
-    );
-  }
-
-}
-
-ImportDataPage.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-
-/**
- * Wrapper component for using unstated
- */
-const ImportDataPageWrapper = (props) => {
-  return createSubscribedElement(ImportDataPage, props, [AppContainer]);
-};
-
-
-export default withTranslation()(ImportDataPageWrapper);

+ 0 - 80
src/client/js/components/Admin/ManageExternalAccount.jsx

@@ -1,80 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import PaginationWrapper from '../PaginationWrapper';
-
-import { createSubscribedElement } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
-import AdminExternalAccountsContainer from '../../services/AdminExternalAccountsContainer';
-import ExternalAccountTable from './Users/ExternalAccountTable';
-import { toastError } from '../../util/apiNotification';
-
-
-class ManageExternalAccount extends React.Component {
-
-  constructor(props) {
-    super(props);
-    this.xss = window.xss;
-    this.handleExternalAccountPage = this.handleExternalAccountPage.bind(this);
-  }
-
-  componentWillMount() {
-    this.handleExternalAccountPage(1);
-  }
-
-  async handleExternalAccountPage(selectedPage) {
-    try {
-      await this.props.adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(selectedPage);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminExternalAccountsContainer } = this.props;
-
-    const pager = (
-      <div className="pull-right">
-        <PaginationWrapper
-          activePage={adminExternalAccountsContainer.state.activePage}
-          changePage={this.handleExternalAccountPage}
-          totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
-          pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
-        />
-      </div>
-    );
-    return (
-      <Fragment>
-        <p>
-          <a className="btn btn-default" href="/admin/users">
-            <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-            { t('user_management.back_to_user_management') }
-          </a>
-        </p>
-
-        <h2>{ t('user_management.external_account_list') }</h2>
-
-        { pager }
-        <ExternalAccountTable />
-        { pager }
-
-      </Fragment>
-    );
-  }
-
-}
-
-ManageExternalAccount.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
-};
-
-const ManageExternalAccountWrapper = (props) => {
-  return createSubscribedElement(ManageExternalAccount, props, [AppContainer, AdminExternalAccountsContainer]);
-};
-
-
-export default withTranslation()(ManageExternalAccountWrapper);

+ 0 - 129
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -1,129 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-const logger = loggerFactory('growi:importer');
-
-class LineBreakForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-
-  async onClickSubmit() {
-    const { t } = this.props;
-
-    try {
-      await this.props.adminMarkDownContainer.updateLineBreakSetting();
-      toastSuccess(t('markdown_setting.updated_lineBreak'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  renderLineBreakOption() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { isEnabledLinebreaks } = adminMarkDownContainer.state;
-
-    const helpLineBreak = { __html: t('markdown_setting.Enable Line Break desc') };
-
-    return (
-      <div className="form-group row">
-        <div className="col-xs-offset-4 col-xs-6 text-left">
-          <div className="checkbox checkbox-success">
-            <input
-              type="checkbox"
-              id="isEnabledLinebreaks"
-              checked={isEnabledLinebreaks}
-              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
-            />
-            <label htmlFor="isEnabledLinebreaks">
-              {t('markdown_setting.Enable Line Break')}
-            </label>
-          </div>
-          <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
-        </div>
-      </div>
-    );
-  }
-
-  renderLineBreakInCommentOption() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
-
-    const helpLineBreakInComment = { __html: t('markdown_setting.Enable Line Break for comment desc') };
-
-    return (
-      <div className="form-group row">
-        <div className="col-xs-offset-4 col-xs-6 text-left">
-          <div className="checkbox checkbox-success">
-            <input
-              type="checkbox"
-              id="isEnabledLinebreaksInComments"
-              checked={isEnabledLinebreaksInComments}
-              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
-            />
-            <label htmlFor="isEnabledLinebreaksInComments">
-              {t('markdown_setting.Enable Line Break for comment')}
-            </label>
-          </div>
-          <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
-        </div>
-      </div>
-    );
-  }
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <fieldset className="row">
-          {this.renderLineBreakOption()}
-          {this.renderLineBreakInCommentOption()}
-        </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}
-              disabled={adminMarkDownContainer.state.retrieveError != null}
-            >
-              {t('Update')}
-            </button>
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const LineBreakFormWrapper = (props) => {
-  return createSubscribedElement(LineBreakForm, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-LineBreakForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-};
-
-export default withTranslation()(LineBreakFormWrapper);

+ 0 - 115
src/client/js/components/Admin/MarkdownSetting/LineBreakSetting.jsx

@@ -1,115 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-const logger = loggerFactory('growi:LineBreak');
-
-class LineBreakSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const { appContainer } = this.props;
-
-    this.state = {
-      isEnabledLinebreaks: appContainer.config.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: appContainer.config.isEnabledLinebreaksInComments,
-    };
-    this.onChangeEnableLineBreaks = this.onChangeEnableLineBreaks.bind(this);
-    this.onChangeEnableLineBreaksInComments = this.onChangeEnableLineBreaksInComments.bind(this);
-    this.changeLineBreakSettings = this.changeLineBreakSettings.bind(this);
-  }
-
-
-  onChangeEnableLineBreaks() {
-    this.setState({ isEnabledLinebreaks: !this.state.isEnabledLinebreaks });
-  }
-
-  onChangeEnableLineBreaksInComments() {
-    this.setState({ isEnabledLinebreaksInComments: !this.state.isEnabledLinebreaksInComments });
-  }
-
-  async changeLineBreakSettings() {
-    const { appContainer } = this.props;
-    const params = {
-      isEnabledLinebreaks: this.state.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
-    };
-    try {
-      await appContainer.apiPost('/admin/markdown/lineBreaksSetting', { params });
-      toastSuccess('Success change line braek setting');
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row my-3">
-          <div className="form-group">
-            <legend>{ t('markdown_setting.line_break_setting') }</legend>
-            <p className="well">{ t('markdown_setting.line_break_setting_desc') }</p>
-            <fieldset className="row">
-              <div className="form-group">
-                <div className="col-xs-4 text-right">
-                  <div className="checkbox checkbox-success" onChange={this.onChangeEnableLineBreaks}>
-                    <input type="checkbox" name="isEnabledLinebreaks" checked={this.state.isEnabledLinebreaks} />
-                    <label>
-                      { t('markdown_setting.Enable Line Break') }
-                    </label>
-                    <p className="help-block">{ t('markdown_setting.Enable Line Break desc') }</p>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-            <fieldset className="row">
-              <div className="form-group my-3">
-                <div className="col-xs-4 text-right">
-                  <div className="checkbox checkbox-success" onChange={this.onChangeEnableLineBreaksInComments}>
-                    <input type="checkbox" name="isEnabledLinebreaksInComments" checked={this.state.isEnabledLinebreaksInComments} />
-                    <label>
-                      { t('markdown_setting.Enable Line Break for comment') }
-                    </label>
-                    <p className="help-block">{ t('markdown_setting.Enable Line Break for comment desc') }</p>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-          </div>
-          <div className="form-group my-3">
-            <div className="col-xs-offset-4 col-xs-5">
-              <button type="submit" className="btn btn-primary" onClick={this.changeLineBreakSettings}>{ t('Update') }</button>
-            </div>
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const LineBreakSettingWrapper = (props) => {
-  return createSubscribedElement(LineBreakSetting, props, [AppContainer]);
-};
-
-LineBreakSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(LineBreakSettingWrapper);

+ 0 - 76
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,76 +0,0 @@
-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;
-
-    return (
-      <React.Fragment>
-        {/* Line Break Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{ t('markdown_setting.line_break_setting') }</h2>
-          <p className="well">{ t('markdown_setting.line_break_setting_desc') }</p>
-          <LineBreakForm />
-        </div>
-
-        {/* Presentation Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{ t('markdown_setting.presentation_setting') }</h2>
-          <p className="well">{ t('markdown_setting.presentation_setting_desc') }</p>
-          <PresentationForm />
-        </div>
-
-        {/* XSS Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{ t('markdown_setting.XSS_setting') }</h2>
-          <p className="well">{ t('markdown_setting.XSS_setting_desc') }</p>
-          <XssForm />
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const MarkdownSettingWrapper = (props) => {
-  return createSubscribedElement(MarkdownSetting, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-MarkdownSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-export default withTranslation()(MarkdownSettingWrapper);

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

@@ -1,122 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-const logger = loggerFactory('growi:markdown:presentation');
-
-class PresentationForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t } = this.props;
-
-    try {
-      await this.props.adminMarkDownContainer.updatePresentationSetting();
-      toastSuccess(t('markdown_setting.updated_presentation'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
-
-    return (
-      <fieldset className="form-group row my-2">
-
-        <label className="col-xs-3 control-label text-right">
-          { t('markdown_setting.Page break setting') }
-        </label>
-
-        <div className="col-xs-3 radio radio-primary">
-          <input
-            type="radio"
-            id="pageBreakOption1"
-            checked={pageBreakSeparator === 1}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
-          />
-          <label htmlFor="pageBreakOption1">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset one separator desc') }
-              <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption2"
-            checked={pageBreakSeparator === 2}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
-          />
-          <label htmlFor="pageBreakOption2">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset two separator desc') }
-              <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption3"
-            checked={pageBreakSeparator === 3}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
-          />
-          <label htmlFor="pageBreakOption3">
-            <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Custom separator desc') }
-              <input
-                className="form-control"
-                defaultValue={pageBreakCustomSeparator}
-                onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
-              />
-            </div>
-          </label>
-        </div>
-
-        <div className="form-group my-3">
-          <div className="col-xs-offset-4 col-xs-5">
-            <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}>{ t('Update') }</div>
-          </div>
-        </div>
-
-      </fieldset>
-    );
-  }
-
-}
-
-const PresentationFormWrapper = (props) => {
-  return createSubscribedElement(PresentationForm, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-PresentationForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-export default withTranslation()(PresentationFormWrapper);

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

@@ -1,86 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-class PresentationLineBreakOptions extends React.Component {
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { pageBreakOption, customRegularExpression } = adminMarkDownContainer.state;
-
-    return (
-      <Fragment>
-        <div className="col-xs-3 radio radio-primary">
-          <input
-            type="radio"
-            id="pageBreakOption1"
-            checked={pageBreakOption === 1}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 1 }) }}
-          />
-          <label htmlFor="pageBreakOption1">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset one separator desc') }
-              <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption2"
-            checked={pageBreakOption === 2}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 2 }) }}
-          />
-          <label htmlFor="pageBreakOption2">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset two separator desc') }
-              <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption3"
-            checked={pageBreakOption === 3}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 3 }) }}
-          />
-          <label htmlFor="pageBreakOption3">
-            <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Custom separator desc') }
-              <input
-                className="form-control"
-                defaultValue={customRegularExpression}
-                onChange={(e) => { adminMarkDownContainer.setState({ customRegularExpression: e.target.value }) }}
-              />
-            </div>
-          </label>
-        </div>
-      </Fragment>
-    );
-  }
-
-}
-
-const PresentationLineBreakOptionsWrapper = (props) => {
-  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-PresentationLineBreakOptions.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-export default withTranslation()(PresentationLineBreakOptionsWrapper);

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

@@ -1,89 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-class WhiteListInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.tagWhiteList = React.createRef();
-    this.attrWhiteList = React.createRef();
-
-    this.onClickRecommendTagButton = this.onClickRecommendTagButton.bind(this);
-    this.onClickRecommendAttrButton = this.onClickRecommendAttrButton.bind(this);
-  }
-
-  onClickRecommendTagButton() {
-    this.tagWhiteList.current.value = tags;
-    this.props.adminMarkDownContainer.setState({ tagWhiteList: tags });
-  }
-
-  onClickRecommendAttrButton() {
-    this.attrWhiteList.current.value = attrs;
-    this.props.adminMarkDownContainer.setState({ attrWhiteList: attrs });
-  }
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-
-    return (
-      <>
-        <div className="m-t-15">
-          <div className="d-flex justify-content-between">
-            {t('markdown_setting.Tag names')}
-            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendTagButton}>
-              {t('markdown_setting.import_recommended', { target: 'Tags' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedTags"
-            rows="6"
-            cols="40"
-            ref={this.tagWhiteList}
-            defaultValue={adminMarkDownContainer.state.tagWhiteList}
-            onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
-          />
-        </div>
-        <div className="m-t-15">
-          <div className="d-flex justify-content-between">
-            {t('markdown_setting.Tag attributes')}
-            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendAttrButton}>
-              {t('markdown_setting.import_recommended', { target: 'Attrs' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedAttrs"
-            rows="6"
-            cols="40"
-            ref={this.attrWhiteList}
-            defaultValue={adminMarkDownContainer.state.attrWhiteList}
-            onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
-          />
-        </div>
-      </>
-    );
-  }
-
-}
-
-const WhiteListWrapper = (props) => {
-  return createSubscribedElement(WhiteListInput, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-WhiteListInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-export default withTranslation()(WhiteListWrapper);

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

@@ -1,163 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-import WhiteListInput from './WhiteListInput';
-
-const logger = loggerFactory('growi:importer');
-
-class XssForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t } = this.props;
-
-    try {
-      await this.props.adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('markdown_setting.updated_xss'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  xssOptions() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { xssOption } = adminMarkDownContainer.state;
-
-    return (
-      <fieldset className="row col-xs-12 my-3">
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption1"
-            name="XssOption"
-            checked={xssOption === 1}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
-          />
-          <label htmlFor="xssOption1">
-            <p className="font-weight-bold">{t('markdown_setting.Ignore all tags')}</p>
-            <div className="m-t-15">
-              {t('markdown_setting.Ignore all tags desc')}
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption2"
-            name="XssOption"
-            checked={xssOption === 2}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
-          />
-          <label htmlFor="xssOption2">
-            <p className="font-weight-bold">{t('markdown_setting.Recommended setting')}</p>
-            <div className="m-t-15">
-              <div className="d-flex justify-content-between">
-                {t('markdown_setting.Tag names')}
-              </div>
-              <textarea
-                className="form-control xss-list"
-                name="recommendedTags"
-                rows="6"
-                cols="40"
-                readOnly
-                defaultValue={tags}
-              />
-            </div>
-            <div className="m-t-15">
-              <div className="d-flex justify-content-between">
-                {t('markdown_setting.Tag attributes')}
-              </div>
-              <textarea
-                className="form-control xss-list"
-                name="recommendedAttrs"
-                rows="6"
-                cols="40"
-                readOnly
-                defaultValue={attrs}
-              />
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption3"
-            name="XssOption"
-            checked={xssOption === 3}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
-          />
-          <label htmlFor="xssOption3">
-            <p className="font-weight-bold">{t('markdown_setting.Custom Whitelist')}</p>
-            <WhiteListInput />
-          </label>
-        </div>
-      </fieldset>
-    );
-  }
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { isEnabledXss } = adminMarkDownContainer.state;
-
-    return (
-      <React.Fragment>
-        <form className="row">
-          <div className="form-group">
-            <div className="col-xs-offset-4 col-xs-4 text-left">
-              <div className="checkbox checkbox-success">
-                <input
-                  type="checkbox"
-                  id="XssEnable"
-                  className="form-check-input"
-                  name="isEnabledXss"
-                  checked={isEnabledXss}
-                  onChange={adminMarkDownContainer.switchEnableXss}
-                />
-                <label htmlFor="XssEnable">
-                  {t('markdown_setting.Enable XSS prevention')}
-                </label>
-              </div>
-            </div>
-            {isEnabledXss && this.xssOptions()}
-          </div>
-          <div className="form-group my-3">
-            <div className="col-xs-offset-4 col-xs-5">
-              <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}> {t('Update')}</div>
-            </div>
-          </div>
-        </form>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const XssFormWrapper = (props) => {
-  return createSubscribedElement(XssForm, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-XssForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-};
-
-export default withTranslation()(XssFormWrapper);

+ 0 - 120
src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx

@@ -1,120 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-class UserGroupCreateForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      name: '',
-    };
-
-    this.xss = window.xss;
-
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  handleChange(event) {
-    const target = event.target;
-    const value = target.type === 'checkbox' ? target.checked : target.value;
-    const name = target.name;
-
-    this.setState({
-      [name]: value,
-    });
-  }
-
-  async handleSubmit(e) {
-    e.preventDefault();
-
-    try {
-      const res = await this.props.appContainer.apiv3.post('/user-groups', {
-        name: this.state.name,
-      });
-
-      const userGroup = res.data.userGroup;
-      const userGroupId = userGroup._id;
-
-      const res2 = await this.props.appContainer.apiv3.get(`/user-groups/${userGroupId}/users`);
-
-      const { users } = res2.data;
-
-      this.props.onCreate(userGroup, users);
-
-      this.setState({ name: '' });
-
-      toastSuccess(`Created a user group "${this.xss.process(userGroup.name)}"`);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  validateForm() {
-    return this.state.name !== '';
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div>
-        <p>
-          {this.props.isAclEnabled
-            ? (
-              <button type="button" data-toggle="collapse" className="btn btn-default" href="#createGroupForm">
-                { t('user_group_management.create_group') }
-              </button>
-            )
-            : (
-              t('user_group_management.deny_create_group')
-            )
-          }
-        </p>
-        <form onSubmit={this.handleSubmit}>
-          <div id="createGroupForm" className="collapse">
-            <div className="form-group">
-              <label htmlFor="name">{ t('user_group_management.group_name') }</label>
-              <textarea
-                id="name"
-                name="name"
-                className="form-control"
-                placeholder={t('user_group_management.group_example')}
-                value={this.state.name}
-                onChange={this.handleChange}
-              >
-              </textarea>
-            </div>
-            <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>{ t('Create') }</button>
-          </div>
-        </form>
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupCreateFormWrapper = (props) => {
-  return createSubscribedElement(UserGroupCreateForm, props, [AppContainer]);
-};
-
-UserGroupCreateForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isAclEnabled: PropTypes.bool,
-  onCreate: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(UserGroupCreateFormWrapper);

+ 0 - 206
src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx

@@ -1,206 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import Modal from 'react-bootstrap/es/Modal';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-/**
- * Delete User Group Select component
- *
- * @export
- * @class GrantSelector
- * @extends {React.Component}
- */
-class UserGroupDeleteModal extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const { t } = this.props;
-
-    // actionName master constants
-    this.actionForPages = {
-      public: 'public',
-      delete: 'delete',
-      transfer: 'transfer',
-    };
-
-    this.availableOptions = [
-      {
-        id: 1, actionForPages: this.actionForPages.public, iconClass: 'icon-people', styleClass: '', label: t('user_group_management.publish_pages'),
-      },
-      {
-        id: 2, actionForPages: this.actionForPages.delete, iconClass: 'icon-trash', styleClass: 'text-danger', label: t('user_group_management.delete_pages'),
-      },
-      {
-        id: 3, actionForPages: this.actionForPages.transfer, iconClass: 'icon-options', styleClass: '', label: t('user_group_management.transfer_pages'),
-      },
-    ];
-
-    this.initialState = {
-      actionName: '',
-      transferToUserGroupId: '',
-    };
-
-    this.state = this.initialState;
-
-    this.xss = window.xss;
-
-    this.onHide = this.onHide.bind(this);
-    this.handleActionChange = this.handleActionChange.bind(this);
-    this.handleGroupChange = this.handleGroupChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.renderPageActionSelector = this.renderPageActionSelector.bind(this);
-    this.renderGroupSelector = this.renderGroupSelector.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  onHide() {
-    this.setState(this.initialState);
-    this.props.onHide();
-  }
-
-  handleActionChange(e) {
-    const actionName = e.target.value;
-    this.setState({ actionName });
-  }
-
-  handleGroupChange(e) {
-    const transferToUserGroupId = e.target.value;
-    this.setState({ transferToUserGroupId });
-  }
-
-  handleSubmit(e) {
-    e.preventDefault();
-
-    this.props.onDelete({
-      deleteGroupId: this.props.deleteUserGroup._id,
-      actionName: this.state.actionName,
-      transferToUserGroupId: this.state.transferToUserGroupId,
-    });
-  }
-
-  renderPageActionSelector() {
-    const { t } = this.props;
-
-    const optoins = this.availableOptions.map((opt) => {
-      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${t(opt.label)}</span>`;
-      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{t(opt.label)}</option>;
-    });
-
-    return (
-      <select
-        name="actionName"
-        className="form-control"
-        placeholder="select"
-        value={this.state.actionName}
-        onChange={this.handleActionChange}
-      >
-        <option value="" disabled>{t('user_group_management.choose_action')}</option>
-        {optoins}
-      </select>
-    );
-  }
-
-  renderGroupSelector() {
-    const { t } = this.props;
-
-    const groups = this.props.userGroups.filter((group) => {
-      return group._id !== this.props.deleteUserGroup._id;
-    });
-
-    const options = groups.map((group) => {
-      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${this.xss.process(group.name)}`;
-      return <option key={group._id} value={group._id} data-content={dataContent}>{this.xss.process(group.name)}</option>;
-    });
-
-    const defaultOptionText = groups.length === 0 ? t('user_group_management.no_groups') : t('user_group_management.select_group');
-
-    return (
-      <select
-        name="transferToUserGroupId"
-        className={`form-control ${this.state.actionName === this.actionForPages.transfer ? '' : 'd-none'}`}
-        value={this.state.transferToUserGroupId}
-        onChange={this.handleGroupChange}
-      >
-        <option value="" disabled>{defaultOptionText}</option>
-        {options}
-      </select>
-    );
-  }
-
-  validateForm() {
-    let isValid = true;
-
-    if (this.state.actionName === '') {
-      isValid = false;
-    }
-    else if (this.state.actionName === this.actionForPages.transfer) {
-      isValid = this.state.transferToUserGroupId !== '';
-    }
-
-    return isValid;
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Modal show={this.props.isShow} onHide={this.onHide}>
-        <Modal.Header className="modal-header bg-danger" closeButton>
-          <Modal.Title>
-            <i className="icon icon-fire"></i> {t('user_group_management.delete_group')}
-          </Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          <div>
-            <span className="font-weight-bold">{t('user_group_management.group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
-          </div>
-          <div className="text-danger mt-5">
-            {t('user_group_management.group_and_pages_not_retrievable')}
-          </div>
-        </Modal.Body>
-        <Modal.Footer>
-          <form className="d-flex justify-content-between" onSubmit={this.handleSubmit}>
-            <div className="d-flex">
-              {this.renderPageActionSelector()}
-              {this.renderGroupSelector()}
-            </div>
-            <button type="submit" value="" className="btn btn-sm btn-danger" disabled={!this.validateForm()}>
-              <i className="icon icon-fire"></i> {t('Delete')}
-            </button>
-          </form>
-        </Modal.Footer>
-      </Modal>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupDeleteModalWrapper = (props) => {
-  return createSubscribedElement(UserGroupDeleteModal, props, [AppContainer]);
-};
-
-UserGroupDeleteModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  userGroups: PropTypes.arrayOf(PropTypes.object).isRequired,
-  deleteUserGroup: PropTypes.object,
-  onDelete: PropTypes.func.isRequired,
-  isShow: PropTypes.bool.isRequired,
-  onShow: PropTypes.func.isRequired,
-  onHide: PropTypes.func.isRequired,
-};
-
-UserGroupDeleteModal.defaultProps = {
-  deleteUserGroup: {},
-};
-
-export default withTranslation()(UserGroupDeleteModalWrapper);

+ 0 - 186
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -1,186 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-
-import PaginationWrapper from '../../PaginationWrapper';
-import UserGroupTable from './UserGroupTable';
-import UserGroupCreateForm from './UserGroupCreateForm';
-import UserGroupDeleteModal from './UserGroupDeleteModal';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-class UserGroupPage extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      userGroups: [],
-      userGroupRelations: {},
-      selectedUserGroup: undefined, // not null but undefined (to use defaultProps in UserGroupDeleteModal)
-      isDeleteModalShow: false,
-      activePage: 1,
-      totalUserGroups: 0,
-      pagingLimit: Infinity,
-    };
-
-    this.xss = window.xss;
-
-    this.handlePage = this.handlePage.bind(this);
-    this.showDeleteModal = this.showDeleteModal.bind(this);
-    this.hideDeleteModal = this.hideDeleteModal.bind(this);
-    this.addUserGroup = this.addUserGroup.bind(this);
-    this.deleteUserGroupById = this.deleteUserGroupById.bind(this);
-  }
-
-  async componentDidMount() {
-    await this.syncUserGroupAndRelations();
-  }
-
-  async showDeleteModal(group) {
-    try {
-      await this.syncUserGroupAndRelations();
-
-      this.setState({
-        selectedUserGroup: group,
-        isDeleteModalShow: true,
-      });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  hideDeleteModal() {
-    this.setState({
-      selectedUserGroup: undefined,
-      isDeleteModalShow: false,
-    });
-  }
-
-  addUserGroup(userGroup, users) {
-    this.setState((prevState) => {
-      const userGroupRelations = Object.assign(prevState.userGroupRelations, {
-        [userGroup._id]: users,
-      });
-
-      return {
-        userGroups: [...prevState.userGroups, userGroup],
-        userGroupRelations,
-      };
-    });
-  }
-
-  async deleteUserGroupById({ deleteGroupId, actionName, transferToUserGroupId }) {
-    try {
-      const res = await this.props.appContainer.apiv3.delete(`/user-groups/${deleteGroupId}`, {
-        actionName,
-        transferToUserGroupId,
-      });
-
-      this.setState((prevState) => {
-        const userGroups = prevState.userGroups.filter((userGroup) => {
-          return userGroup._id !== deleteGroupId;
-        });
-
-        delete prevState.userGroupRelations[deleteGroupId];
-
-        return {
-          userGroups,
-          userGroupRelations: prevState.userGroupRelations,
-          selectedUserGroup: undefined,
-          isDeleteModalShow: false,
-        };
-      });
-
-      toastSuccess(`Deleted a group "${this.xss.process(res.data.userGroup.name)}"`);
-    }
-    catch (err) {
-      toastError(new Error('Unable to delete the group'));
-    }
-  }
-
-  async handlePage(selectedPage) {
-    await this.setState({ activePage: selectedPage });
-    await this.syncUserGroupAndRelations();
-  }
-
-  async syncUserGroupAndRelations() {
-    let userGroups = [];
-    let userGroupRelations = {};
-    let totalUserGroups = 0;
-    let pagingLimit = Infinity;
-
-    try {
-      const params = { page: this.state.activePage };
-      const responses = await Promise.all([
-        this.props.appContainer.apiv3.get('/user-groups', params),
-        this.props.appContainer.apiv3.get('/user-group-relations', params),
-      ]);
-
-      const [userGroupsRes, userGroupRelationsRes] = responses;
-      userGroups = userGroupsRes.data.userGroups;
-      totalUserGroups = userGroupsRes.data.totalUserGroups;
-      pagingLimit = userGroupsRes.data.pagingLimit;
-      userGroupRelations = userGroupRelationsRes.data.userGroupRelations;
-
-      this.setState({
-        userGroups,
-        userGroupRelations,
-        totalUserGroups,
-        pagingLimit,
-      });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    return (
-      <Fragment>
-        <UserGroupCreateForm
-          isAclEnabled={this.props.isAclEnabled}
-          onCreate={this.addUserGroup}
-        />
-        <UserGroupTable
-          userGroups={this.state.userGroups}
-          isAclEnabled={this.props.isAclEnabled}
-          onDelete={this.showDeleteModal}
-          userGroupRelations={this.state.userGroupRelations}
-        />
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePage}
-          totalItemsCount={this.state.totalUserGroups}
-          pagingLimit={this.state.pagingLimit}
-        />
-        <UserGroupDeleteModal
-          userGroups={this.state.userGroups}
-          deleteUserGroup={this.state.selectedUserGroup}
-          onDelete={this.deleteUserGroupById}
-          isShow={this.state.isDeleteModalShow}
-          onShow={this.showDeleteModal}
-          onHide={this.hideDeleteModal}
-        />
-      </Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupPageWrapper = (props) => {
-  return createSubscribedElement(UserGroupPage, props, [AppContainer]);
-};
-
-UserGroupPage.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isAclEnabled: PropTypes.bool,
-};
-
-export default UserGroupPageWrapper;

+ 0 - 134
src/client/js/components/Admin/UserGroup/UserGroupTable.jsx

@@ -1,134 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import dateFnsFormat from 'date-fns/format';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class UserGroupTable extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.xss = window.xss;
-
-    this.state = {
-      userGroups: this.props.userGroups,
-      userGroupRelations: this.props.userGroupRelations,
-    };
-
-    this.onDelete = this.onDelete.bind(this);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.setState({
-      userGroups: nextProps.userGroups,
-      userGroupRelations: nextProps.userGroupRelations,
-    });
-  }
-
-  onDelete(e) {
-    const { target } = e;
-    const groupId = target.getAttribute('data-user-group-id');
-    const group = this.state.userGroups.find((group) => {
-      return group._id === groupId;
-    });
-
-    this.props.onDelete(group);
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
-        <h2>{t('user_group_management.group_list')}</h2>
-
-        <table className="table table-bordered table-user-list">
-          <thead>
-            <tr>
-              <th>{ t('Name') }</th>
-              <th>{ t('User') }</th>
-              <th width="100px">{ t('Created') }</th>
-              <th width="70px"></th>
-            </tr>
-          </thead>
-          <tbody>
-            {this.state.userGroups.map((group) => {
-              return (
-                <tr key={group._id}>
-                  {this.props.isAclEnabled
-                    ? (
-                      <td><a href={`/admin/user-group-detail/${group._id}`}>{this.xss.process(group.name)}</a></td>
-                    )
-                    : (
-                      <td>{this.xss.process(group.name)}</td>
-                    )
-                  }
-                  <td>
-                    <ul className="list-inline">
-                      {this.state.userGroupRelations[group._id].map((user) => {
-                        return <li key={user._id} className="list-inline-item badge badge-primary">{this.xss.process(user.username)}</li>;
-                      })}
-                    </ul>
-                  </td>
-                  <td>{dateFnsFormat(new Date(group.createdAt), 'yyyy-MM-dd')}</td>
-                  {this.props.isAclEnabled
-                    ? (
-                      <td>
-                        <div className="btn-group admin-group-menu">
-                          <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                            <i className="icon-settings"></i> <span className="caret"></span>
-                          </button>
-                          <ul className="dropdown-menu" role="menu">
-                            <li>
-                              <a href={`/admin/user-group-detail/${group._id}`}>
-                                <i className="icon-fw icon-note"></i> { t('Edit') }
-                              </a>
-                            </li>
-
-                            <li>
-                              <a href="#" onClick={this.onDelete} data-user-group-id={group._id}>
-                                <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
-                              </a>
-                            </li>
-
-                          </ul>
-                        </div>
-                      </td>
-                    )
-                    : (
-                      <td></td>
-                    )
-                  }
-                </tr>
-              );
-            })}
-          </tbody>
-        </table>
-      </Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupTableWrapper = (props) => {
-  return createSubscribedElement(UserGroupTable, props, [AppContainer]);
-};
-
-
-UserGroupTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  userGroups: PropTypes.arrayOf(PropTypes.object).isRequired,
-  userGroupRelations: PropTypes.object.isRequired,
-  isAclEnabled: PropTypes.bool,
-  onDelete: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(UserGroupTableWrapper);

+ 0 - 37
src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx

@@ -1,37 +0,0 @@
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CheckBoxForSerchUserOption extends React.Component {
-
-  render() {
-    const { t, option } = this.props;
-    return (
-      <div className="checkbox checkbox-info" key={`isAlso${option}Searched`}>
-        <input
-          type="checkbox"
-          id={`isAlso${option}Searched`}
-          className="form-check-input"
-          checked={this.props.checked}
-          onChange={this.props.onChange}
-        />
-        <label className="text-capitalize form-check-label ml-3" htmlFor={`isAlso${option}Searched`}>
-          {t('user_group_management.enable_option', { option })}
-        </label>
-      </div>
-    );
-  }
-
-}
-
-
-CheckBoxForSerchUserOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  option: PropTypes.string.isRequired,
-  checked: PropTypes.bool.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(CheckBoxForSerchUserOption);

+ 0 - 37
src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx

@@ -1,37 +0,0 @@
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class RadioButtonForSerchUserOption extends React.Component {
-
-  render() {
-    const { t, searchType } = this.props;
-    return (
-      <div className="radio" key={`${searchType}Match`}>
-        <input
-          type="radio"
-          id={`${searchType}Match`}
-          className="form-check-radio"
-          checked={this.props.checked}
-          onChange={this.props.onChange}
-        />
-        <label className="text-capitalize form-check-label ml-3" htmlFor={`${searchType}Match`}>
-          {t(`user_group_management.${searchType}_match`)}
-        </label>
-      </div>
-    );
-  }
-
-}
-
-
-RadioButtonForSerchUserOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  searchType: PropTypes.string.isRequired,
-  checked: PropTypes.bool.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(RadioButtonForSerchUserOption);

+ 0 - 51
src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx

@@ -1,51 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import UserGroupEditForm from './UserGroupEditForm';
-import UserGroupUserTable from './UserGroupUserTable';
-import UserGroupUserModal from './UserGroupUserModal';
-import UserGroupPageList from './UserGroupPageList';
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class UserGroupDetailPage extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div>
-        <a href="/admin/user-groups" className="btn btn-default">
-          <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-          {t('user_group_management.back_to_list')}
-        </a>
-        <div className="m-t-20 form-box">
-          <UserGroupEditForm />
-        </div>
-        <legend className="m-t-20">{ t('user_group_management.user_list') }</legend>
-        <UserGroupUserTable />
-        <UserGroupUserModal />
-        <legend className="m-t-20">{ t('Page') }</legend>
-        <div className="page-list">
-          <UserGroupPageList />
-        </div>
-      </div>
-    );
-  }
-
-}
-
-UserGroupDetailPage.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupDetailPageWrapper = (props) => {
-  return createSubscribedElement(UserGroupDetailPage, props, [AppContainer]);
-};
-
-export default withTranslation()(UserGroupDetailPageWrapper);

+ 0 - 106
src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -1,106 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import dateFnsFormat from 'date-fns/format';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-class UserGroupEditForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      name: props.userGroupDetailContainer.state.userGroup.name,
-      nameCache: props.userGroupDetailContainer.state.userGroup.name, // cache for name. update every submit
-    };
-
-    this.xss = window.xss;
-
-    this.changeUserGroupName = this.changeUserGroupName.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  changeUserGroupName(event) {
-    this.setState({
-      name: event.target.value,
-    });
-  }
-
-  async handleSubmit(e) {
-    e.preventDefault();
-
-    try {
-      const res = await this.props.userGroupDetailContainer.updateUserGroup({
-        name: this.state.name,
-      });
-
-      toastSuccess(`Updated the group name to "${this.xss.process(res.data.userGroup.name)}"`);
-      this.setState({ nameCache: this.state.name });
-    }
-    catch (err) {
-      toastError(new Error('Unable to update the group name'));
-    }
-  }
-
-  validateForm() {
-    return (
-      this.state.name !== this.state.nameCache
-      && this.state.name !== ''
-    );
-  }
-
-  render() {
-    const { t, userGroupDetailContainer } = this.props;
-
-    return (
-      <form className="form-horizontal" onSubmit={this.handleSubmit}>
-        <fieldset>
-          <legend>{ t('user_group_management.basic_info') }</legend>
-          <div className="form-group">
-            <label htmlFor="name" className="col-sm-2 control-label">{ t('Name') }</label>
-            <div className="col-sm-4">
-              <input className="form-control" type="text" name="name" value={this.state.name} onChange={this.changeUserGroupName} />
-            </div>
-          </div>
-          <div className="form-group">
-            <label className="col-sm-2 control-label">{ t('Created') }</label>
-            <div className="col-sm-4">
-              <input
-                type="text"
-                className="form-control"
-                value={dateFnsFormat(new Date(userGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
-                disabled
-              />
-            </div>
-          </div>
-          <div className="form-group">
-            <div className="col-sm-offset-2 col-sm-10">
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>{ t('Update') }</button>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-    );
-  }
-
-}
-
-UserGroupEditForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupEditFormWrapper = (props) => {
-  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, UserGroupDetailContainer]);
-};
-
-export default withTranslation()(UserGroupEditFormWrapper);

+ 0 - 87
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -1,87 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import Page from '../../PageList/Page';
-import PaginationWrapper from '../../PaginationWrapper';
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
-import { toastError } from '../../../util/apiNotification';
-
-class UserGroupPageList extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      currentPages: [],
-      activePage: 1,
-      total: 0,
-      pagingLimit: 10,
-    };
-
-    this.handlePageChange = this.handlePageChange.bind(this);
-  }
-
-  async componentDidMount() {
-    await this.handlePageChange(this.state.activePage);
-  }
-
-  async handlePageChange(pageNum) {
-    const limit = this.state.pagingLimit;
-    const offset = (pageNum - 1) * limit;
-
-    try {
-      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.userGroupDetailContainer.state.userGroup._id}/pages`, {
-        limit,
-        offset,
-      });
-      const { total, pages } = res.data;
-
-      this.setState({
-        total: total || 0,
-        activePage: pageNum,
-        currentPages: pages,
-      });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, userGroupDetailContainer } = this.props;
-
-    return (
-      <Fragment>
-        <ul className="page-list-ul page-list-ul-flat">
-          {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
-        </ul>
-        {userGroupDetailContainer.state.relatedPages.length === 0 ? <p>{ t('user_group_management.no_pages') }</p> : null}
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePageChange}
-          totalItemsCount={this.state.total}
-          pagingLimit={this.state.pagingLimit}
-        />
-      </Fragment>
-    );
-  }
-
-}
-
-UserGroupPageList.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupPageListWrapper = (props) => {
-  return createSubscribedElement(UserGroupPageList, props, [AppContainer, UserGroupDetailContainer]);
-};
-
-export default withTranslation()(UserGroupPageListWrapper);

+ 0 - 164
src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -1,164 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-import { debounce } from 'throttle-debounce';
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-import UserPicture from '../../User/UserPicture';
-
-class UserGroupUserFormByInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      keyword: '',
-      inputUser: '',
-      applicableUsers: [],
-      isLoading: false,
-      searchError: null,
-    };
-
-    this.xss = window.xss;
-
-    this.addUserBySubmit = this.addUserBySubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSearch = this.handleSearch.bind(this);
-    this.onKeyDown = this.onKeyDown.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-
-    this.searhApplicableUsersDebounce = debounce(1000, this.searhApplicableUsers);
-  }
-
-  async addUserBySubmit() {
-    if (this.state.inputUser.length === 0) { return }
-    const userName = this.state.inputUser[0].username;
-
-    try {
-      await this.props.userGroupDetailContainer.addUserByUsername(userName);
-      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
-      this.setState({ inputUser: '' });
-    }
-    catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
-    }
-  }
-
-  validateForm() {
-    return this.state.inputUser !== '';
-  }
-
-  async searhApplicableUsers() {
-    try {
-      const users = await this.props.userGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
-      this.setState({ applicableUsers: users, isLoading: false });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  /**
-   * Reflect when forecast is clicked
-   * @param {object} inputUser
-   */
-  handleChange(inputUser) {
-    this.setState({ inputUser });
-  }
-
-  handleSearch(keyword) {
-
-    if (keyword === '') {
-      return;
-    }
-
-    this.setState({ keyword, isLoading: true });
-    this.searhApplicableUsersDebounce();
-  }
-
-  onKeyDown(event) {
-    // 13 is Enter key
-    if (event.keyCode === 13) {
-      this.addUserBySubmit();
-    }
-  }
-
-  renderMenuItemChildren(option) {
-    const { userGroupDetailContainer } = this.props;
-    const user = option;
-    return (
-      <React.Fragment>
-        <UserPicture user={user} size="sm" withoutLink />
-        <strong className="ml-2">{user.username}</strong>
-        {userGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
-        {userGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
-      </React.Fragment>
-    );
-  }
-
-  getEmptyLabel() {
-    return (this.state.searchError !== null) && 'Error on searching.';
-  }
-
-  render() {
-    const { t } = this.props;
-
-    const inputProps = { autoComplete: 'off' };
-
-    return (
-      <div className="row">
-        <div className="col-xs-8 pr-0">
-          <AsyncTypeahead
-            {...this.props}
-            id="name-typeahead-asynctypeahead"
-            ref={(c) => { this.typeahead = c }}
-            inputProps={inputProps}
-            isLoading={this.state.isLoading}
-            labelKey={user => `${user.username} ${user.name} ${user.email}`}
-            minLength={0}
-            options={this.state.applicableUsers} // Search result
-            searchText={(this.state.isLoading ? 'Searching...' : this.getEmptyLabel())}
-            renderMenuItemChildren={this.renderMenuItemChildren}
-            align="left"
-            onChange={this.handleChange}
-            onSearch={this.handleSearch}
-            onKeyDown={this.onKeyDown}
-            caseSensitive={false}
-            clearButton
-          />
-        </div>
-        <div className="col-xs-2 pl-0">
-          <button
-            type="button"
-            className="btn btn-sm btn-success"
-            disabled={!this.validateForm()}
-            onClick={this.addUserBySubmit}
-          >
-            {t('add')}
-          </button>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-UserGroupUserFormByInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupUserFormByInputWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, UserGroupDetailContainer]);
-};
-
-export default withTranslation()(UserGroupUserFormByInputWrapper);

+ 0 - 89
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -1,89 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
-
-import UserGroupUserFormByInput from './UserGroupUserFormByInput';
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
-import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
-import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
-
-class UserGroupUserModal extends React.Component {
-
-  render() {
-    const { t, userGroupDetailContainer } = this.props;
-
-    return (
-      <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
-        <Modal.Header closeButton>
-          <Modal.Title>{t('user_group_management.add_user')}</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          <div className="p-3">
-            <UserGroupUserFormByInput />
-          </div>
-          <h2 className="border-bottom">{t('user_group_management.search_option')}</h2>
-          <div className="row mt-4">
-            <div className="col-xs-6">
-              <div className="mb-5">
-                <CheckBoxForSerchUserOption
-                  option="Mail"
-                  checked={userGroupDetailContainer.state.isAlsoMailSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoMailSearched}
-                />
-              </div>
-              <div className="mb-5">
-                <CheckBoxForSerchUserOption
-                  option="Name"
-                  checked={userGroupDetailContainer.state.isAlsoNameSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoNameSearched}
-                />
-              </div>
-            </div>
-            <div className="col-xs-6">
-              <div className="mb-5">
-                <RadioButtonForSerchUserOption
-                  searchType="forward"
-                  checked={userGroupDetailContainer.state.searchType === 'forward'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('forward') }}
-                />
-              </div>
-              <div className="mb-5">
-                <RadioButtonForSerchUserOption
-                  searchType="partial"
-                  checked={userGroupDetailContainer.state.searchType === 'partial'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('partial') }}
-                />
-              </div>
-              <div className="mb-5">
-                <RadioButtonForSerchUserOption
-                  searchType="backward"
-                  checked={userGroupDetailContainer.state.searchType === 'backword'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('backword') }}
-                />
-              </div>
-            </div>
-          </div>
-        </Modal.Body>
-      </Modal>
-    );
-  }
-
-}
-
-UserGroupUserModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupUserModalWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, UserGroupDetailContainer]);
-};
-
-export default withTranslation()(UserGroupUserModalWrapper);

Some files were not shown because too many files changed in this diff