Просмотр исходного кода

Merge pull request #7409 from arafubeatbox/support/merge-master-to-questionnaire-branch

Support/merge master to questionnaire branch
Ryoji Shimizu 3 лет назад
Родитель
Сommit
fabbe5bdee
100 измененных файлов с 1015 добавлено и 1182 удалено
  1. 5 2
      .github/ISSUE_TEMPLATE/config.yml
  2. 0 25
      .github/ISSUE_TEMPLATE/user-request.md
  3. 3 0
      .github/workflows/ci-app-prod.yml
  4. 2 0
      .github/workflows/ci-app.yml
  5. 1 2
      .github/workflows/draft-release.yml
  6. 1 1
      .github/workflows/pr-to-master.yml
  7. 1 1
      .github/workflows/release-slackbot-proxy.yml
  8. 77 1
      CHANGELOG.md
  9. 85 0
      bin/data-migrations/v6/README.md
  10. 57 0
      bin/data-migrations/v6/src/migration.js
  11. 75 0
      bin/data-migrations/v6/src/processor.js
  12. 1 1
      lerna.json
  13. 1 1
      package.json
  14. 2 1
      packages/app/config/next-i18next.config.ts
  15. 1 0
      packages/app/docker/Dockerfile
  16. 1 1
      packages/app/docker/README.md
  17. 16 16
      packages/app/package.json
  18. 4 16
      packages/app/public/static/locales/en_US/admin.json
  19. 1 1
      packages/app/public/static/locales/en_US/commons.json
  20. 1 2
      packages/app/public/static/locales/en_US/translation.json
  21. 4 16
      packages/app/public/static/locales/ja_JP/admin.json
  22. 2 2
      packages/app/public/static/locales/ja_JP/commons.json
  23. 1 2
      packages/app/public/static/locales/ja_JP/translation.json
  24. 4 16
      packages/app/public/static/locales/zh_CN/admin.json
  25. 1 1
      packages/app/public/static/locales/zh_CN/commons.json
  26. 1 2
      packages/app/public/static/locales/zh_CN/translation.json
  27. 6 6
      packages/app/resource/locales/en_US/admin/userInvitation.txt
  28. 11 0
      packages/app/resource/locales/en_US/admin/userResetPassword.txt
  29. 8 8
      packages/app/resource/locales/en_US/admin/userWaitingActivation.txt
  30. 3 3
      packages/app/resource/locales/en_US/notifications/comment.txt
  31. 4 4
      packages/app/resource/locales/en_US/notifications/notActiveUser.txt
  32. 2 2
      packages/app/resource/locales/en_US/notifications/pageCreate.txt
  33. 2 2
      packages/app/resource/locales/en_US/notifications/pageDelete.txt
  34. 2 2
      packages/app/resource/locales/en_US/notifications/pageEdit.txt
  35. 2 2
      packages/app/resource/locales/en_US/notifications/pageLike.txt
  36. 2 2
      packages/app/resource/locales/en_US/notifications/pageMove.txt
  37. 4 4
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  38. 1 1
      packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt
  39. 4 4
      packages/app/resource/locales/en_US/notifications/userActivation.txt
  40. 6 6
      packages/app/resource/locales/ja_JP/admin/userInvitation.txt
  41. 12 0
      packages/app/resource/locales/ja_JP/admin/userResetPassword.txt
  42. 8 8
      packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt
  43. 4 4
      packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt
  44. 4 4
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  45. 1 1
      packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt
  46. 4 4
      packages/app/resource/locales/ja_JP/notifications/userActivation.txt
  47. 6 6
      packages/app/resource/locales/zh_CN/admin/userInvitation.txt
  48. 11 0
      packages/app/resource/locales/zh_CN/admin/userResetPassword.txt
  49. 8 8
      packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt
  50. 3 3
      packages/app/resource/locales/zh_CN/notifications/comment.txt
  51. 4 4
      packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt
  52. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageCreate.txt
  53. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageDelete.txt
  54. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageEdit.txt
  55. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageLike.txt
  56. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageMove.txt
  57. 3 3
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  58. 1 1
      packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt
  59. 4 4
      packages/app/resource/locales/zh_CN/notifications/userActivation.txt
  60. 0 42
      packages/app/src/client/legacy/crowi-presentation.js
  61. 0 1
      packages/app/src/client/services/AdminAppContainer.js
  62. 0 35
      packages/app/src/client/services/AdminMarkDownContainer.js
  63. 5 11
      packages/app/src/client/services/layout.ts
  64. 2 2
      packages/app/src/client/services/page-operation.ts
  65. 89 0
      packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  66. 33 0
      packages/app/src/client/services/side-effects/hackmd-draft-updated.ts
  67. 89 0
      packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  68. 3 6
      packages/app/src/client/services/side-effects/hash-changed.ts
  69. 39 0
      packages/app/src/client/services/side-effects/page-updated.ts
  70. 1 6
      packages/app/src/client/services/user-ui-settings.ts
  71. 60 0
      packages/app/src/client/util/locale-utils.ts
  72. 0 107
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  73. 0 379
      packages/app/src/client/util/reveal/plugins/markdown.js
  74. 0 5
      packages/app/src/client/util/smooth-scroll.ts
  75. 1 1
      packages/app/src/client/util/toastr.ts
  76. 1 1
      packages/app/src/components/Admin/App/AwsSetting.tsx
  77. 9 8
      packages/app/src/components/Admin/App/ConfirmModal.tsx
  78. 3 3
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  79. 0 8
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  80. 0 143
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  81. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  82. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  83. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  84. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  85. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  86. 5 11
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  87. 62 27
      packages/app/src/components/Comments.tsx
  88. 37 0
      packages/app/src/components/Common/LazyRenderer.tsx
  89. 2 5
      packages/app/src/components/ContentLinkButtons.tsx
  90. 7 22
      packages/app/src/components/CustomNavigation/CustomTabContent.tsx
  91. 13 46
      packages/app/src/components/DescendantsPageList.tsx
  92. 13 9
      packages/app/src/components/DescendantsPageListModal.tsx
  93. 1 2
      packages/app/src/components/Fab.tsx
  94. 0 14
      packages/app/src/components/Invited.module.scss
  95. 0 11
      packages/app/src/components/Layout/BasicLayout.tsx
  96. 1 1
      packages/app/src/components/Layout/MainPane.tsx
  97. 0 5
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  98. 1 6
      packages/app/src/components/LoginForm.tsx
  99. 1 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  100. 44 39
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

+ 5 - 2
.github/ISSUE_TEMPLATE/config.yml

@@ -1,5 +1,8 @@
 blank_issues_enabled: false
 contact_links:
-  - name: Question or Suggestions
+  - name: User request or Suggestions
+    url: https://github.com/weseek/growi/discussions
+    about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
+  - name: Questions
     url: https://growi-slackin.weseek.co.jp/
-    about: If you have questions or suggestions, you can join our Slack team and talk about anything, anytime.
+    about: If you have questions, you can join our Slack team and talk about anything, anytime.

+ 0 - 25
.github/ISSUE_TEMPLATE/user-request.md

@@ -1,25 +0,0 @@
----
-name: User request
-about: Suggest an idea for this project
-title: 'Request:'
-labels: user requests
----
-
-
-## Informations
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. 
-
-e.g. I'm having trouble getting immediate access to information
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to realization.
-
-e.g. It's good if there is a space where everyone can access information in common.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-- [ ] Custom Page In Sidebar
-  - [ ] Can be edited in `/Sidebar` path
-  - [ ] Can be described with md like a page

+ 3 - 0
.github/workflows/ci-app-prod.yml

@@ -14,6 +14,8 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
+      - packages/presentation/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
@@ -31,6 +33,7 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 2 - 0
.github/workflows/ci-app.yml

@@ -14,6 +14,8 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
+      - packages/presentation/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 1 - 2
.github/workflows/draft-release.yml

@@ -55,9 +55,8 @@ jobs:
           RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
           echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT
 
-      # See: https://github.com/bakunyo/git-pr-release-action/issues/15, https://github.com/samunohito/SimpleVolumeMixer/commit/2059044c71236509466cf9b1bb2d56d515274938
       - name: Create/Update Pull Request
-        uses: bakunyo/git-pr-release-action@281e1fe424fac01f3992542266805e4202a22fe0
+        uses: bakunyo/git-pr-release-action@master
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current

+ 1 - 1
.github/workflows/pr-to-master.yml

@@ -36,7 +36,7 @@ jobs:
         !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
-      - uses: amannn/action-semantic-pull-request@v4.2.0
+      - uses: amannn/action-semantic-pull-request@v5.0.2
         with:
           types: |
             feat

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -58,7 +58,7 @@ jobs:
       uses: docker/setup-buildx-action@v2
 
     - name: Build and push
-      uses: docker/build-push-action@v2
+      uses: docker/build-push-action@v4
       with:
         context: .
         file: ./packages/slackbot-proxy/docker/Dockerfile

+ 77 - 1
CHANGELOG.md

@@ -1,9 +1,85 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.3...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.6...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.0.6](https://github.com/weseek/growi/compare/v6.0.5...v6.0.6) - 2023-02-14
+
+### 💎 Features
+
+- feat: Presentation (#7367) @yuki-takei
+- feat: Detect i18n locale from browser accept languages (#7341) @jam411
+- feat: Server Side Rendering (#7352) @yuki-takei
+
+### 🚀 Improvement
+
+- imprv: Allow iframe tag (#7368) @yuki-takei
+- imprv: User data serialization (#7355) @miya
+- imprv: Anchor link (#7354) @yuki-takei
+- imprv: classname for fluid layout (#7353) @yuki-takei
+- imprv: Disable lsx in shared page (#7333) @miya
+- imprv: Data mutation (#7336) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Make collapse work for anchor tags (#7381) @jam411
+- fix: Revision short body is not displayed on search results page (#7373) @miya
+- fix: Error when clicking on a page you are not authorized to view on the search results page (#7343) @miya
+- fix: Omit S3 credentials from the response for /_api/v3/app-settings (#7369) @miya
+- fix: Omit S3 credentials from the response for /_api/v3/app-settings (#7369) @miya
+- fix: Keep showing page restricted alert (#7371) @yukendev
+- fix: Recent changes and Timeline (#7366) @yuki-takei
+- fix: Login screen background (#7350) @ayaka0417
+- fix: Comment form background (#7365) @ayaka0417
+- fix: Scroll into view by anchor (#7360) @yuki-takei
+- fix: Routing after creating page with shortcut (#7359) @yuki-takei
+- fix: Border-color in edit mode (#7349) @ayaka0417
+- fix: Can't controll slack notification switch in editor (#7332) @yukendev
+- fix: Show load latest revision button when update drawio or table from view (#7324) @yukendev
+- fix: Can delete own user (#7321) @miya
+- fix: Request to "/_api/v3/page/is-grant-normalized" occurs when in guest mode (#7313) @miya
+
+### 🧰 Maintenance
+
+- support: create README.md for v6 migration (#7380) @yukendev
+- support: Bump SWR to v2.0.3 (#7362) @yuki-takei
+- feat: Refactor common processes in Next Page (#7357) @yukendev
+- support: Migrate Notation for v5 to v6 (#7326) @yukendev
+- ci(deps-dev): bump sass from 1.53.0 to 1.57.1 (#7223) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 4.2.0 to 5.0.2 (#7338) @dependabot
+- ci(deps): bump bakunyo/git-pr-release-action from 281e1fe424fac01f3992542266805e4202a22fe0 to master (#7340) @dependabot
+- ci(deps): bump docker/build-push-action from 2 to 4 (#7339) @dependabot
+- ci(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 (#7344) @dependabot
+- support: Bump SWR to v2 (#7318) @yuki-takei
+- support: Add test for View and Edit contents when saving (#7323) @yukendev
+
+## [v6.0.5](https://github.com/weseek/growi/compare/v6.0.4...v6.0.5) - 2023-01-30
+
+### 🚀 Improvement
+
+- imprv: Override process for CommonSanitizeOptions (#7305) @miya
+
+### 🐛 Bug Fixes
+
+- fix:  Request to "/_api/v3/personal-settings"  occurs when in guest mode (#7307) @miya
+- fix: Undeleteable trash pages when clicked empty trash button bug (#7250) @jam411
+- fix: Guest users are able to move to pages that require authentication (#7300) @miya
+- fix: Modal does not close after clicking on path in DescendantsPageListModal (#7291) @miya
+- fix: GrowiContextualSubNavigation style is broken (#7304) @jam411
+- fix: Markdown in the editor reverted when save with shortcut (#7301) @yukendev
+
+## [v6.0.4](https://github.com/weseek/growi/compare/v6.0.3...v6.0.4) - 2023-01-25
+
+### 🐛 Bug Fixes
+
+- fix: Invalid URL in markdown breaks browser (#7292) @yuki-takei
+- fix: Previous editing markdown remains after changing page (#7285) @yukendev
+
+### 🧰 Maintenance
+
+- ci(deps): bump ua-parser-js from 0.7.31 to 0.7.33 (#7293) @dependabot
+
 ## [v6.0.3](https://github.com/weseek/growi/compare/v6.0.2...v6.0.3) - 2023-01-24
 
 ### 💎 Features

+ 85 - 0
bin/data-migrations/v6/README.md

@@ -0,0 +1,85 @@
+# Migration to v6 from v5
+
+> **Warning**
+> **Migration in this way is applied only to the latest revision. Past revisions are not applied.**
+
+## Usage
+```
+git clone https://github.com/weseek/growi
+cd growi/bin/data-migrations/v6
+
+NETWORK=growi_devcontainer_default \
+MONGO_URI=mongodb://growi_devcontainer_mongo_1/growi \
+docker run --rm \
+  --network $NETWORK \
+  -v "$(pwd)"/src:/opt \
+  -w /opt \
+  -e MIGRATION_TYPE=v6 \
+  mongo:6.0 \
+  /bin/mongosh $MONGO_URI migration.js
+```
+
+## Variables
+| Variable              | Description                                                                    | Default |
+| --------------------- | ------------------------------------------------------------------------------ | ------- |
+| NETWORK     | Network in docker compose of MongoDB server                                                         | -       |
+| MONGO_URI| URI that can connect to MongoDB                                                     | -       |
+
+## Environment variables
+### Required
+
+| Variable              | Description                                                                    | Default |
+| --------------------- | ------------------------------------------------------------------------------ | ------- |
+| MIGRATION_TYPE     | Migrated notation                                                        | -       |
+
+The value of `MIGRATION_TYPE` is one of the following.
+- `v6-drawio`: Migration for Draw.io notation only([
+reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4-draw-io-diagrams-net-%E8%A8%98%E6%B3%95))
+- `v6-plantuml`: Migration for PlantUML notation only([
+reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4-plantuml-%E8%A8%98%E6%B3%95))
+- `v6-tsv`: Migration for table notation by TSV only([
+reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4-csv-tsv-%E3%81%AB%E3%82%88%E3%82%8B%E3%83%86%E3%83%BC%E3%83%95%E3%82%99%E3%83%AB%E6%8F%8F%E7%94%BB%E8%A8%98%E6%B3%95))
+- `v6-csv`: Migration for table notation by CSV only([
+reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4-csv-tsv-%E3%81%AB%E3%82%88%E3%82%8B%E3%83%86%E3%83%BC%E3%83%95%E3%82%99%E3%83%AB%E6%8F%8F%E7%94%BB%E8%A8%98%E6%B3%95))
+- `v6-bracketlink`: Migration for only page links within GROWI([
+reference](https://docs.growi.org/ja/admin-guide/upgrading/60x.html#%E6%9C%AA%E5%AE%9F%E8%A3%85-%E5%BB%83%E6%AD%A2%E6%A4%9C%E8%A8%8E%E4%B8%AD-growi-%E7%8B%AC%E8%87%AA%E8%A8%98%E6%B3%95%E3%81%AE%E3%83%98%E3%82%9A%E3%83%BC%E3%82%B7%E3%82%99%E3%83%AA%E3%83%B3%E3%82%AF))
+- `v6`: Migration for all the above notations
+- `custom`: You can define your own processors and apply them to `revision` (see "Advanced" below for details)
+
+### Optional
+
+| Variable              | Description                                                                    | Default |
+| --------------------- | ------------------------------------------------------------------------------ | ------- |
+| BATCH_SIZE     | Number of revisions to be processed at one time(revision)                                                         | 100       |
+| BATCH_INTERVAL| Interval after batch processing(ms)                                                     | 3000       |
+
+※The `BATCH_INTERVAL` is for server load control. If you don't mind the load of the MongoDB, there is no problem to reduce it.
+
+## Advanced
+
+By creating a function in `growi/bin/data-migrations/v6/src/processor.js` that replaces a specific regular expression, you can replace all specific strings in the latest revisions for all pages.
+
+The following function replaces the string `foo` with the string `bar`.
+
+``` javascript
+function customProcessor(body) {
+  var fooRegExp = /foo/g; // foo regex
+  return body.replace(fooRegExp, 'bar'); // replace to bar
+}
+```
+
+By passing `custom` in the environment variable `MIGRATION_TYPE` and executing it, you can apply the `customProcessor` to all the latest `revisions`.
+```
+git clone https://github.com/weseek/growi
+cd growi/bin/data-migrations/v6
+
+NETWORK=growi_devcontainer_default \
+MONGO_URI=mongodb://growi_devcontainer_mongo_1/growi \
+docker run --rm \
+  --network $NETWORK \
+  -v "$(pwd)"/src:/opt \
+  -w /opt \
+  -e MIGRATION_TYPE=custom \
+  mongo:6.0 \
+  /bin/mongosh $MONGO_URI migration.js
+```

+ 57 - 0
bin/data-migrations/v6/src/migration.js

@@ -0,0 +1,57 @@
+
+/* eslint-disable no-undef, no-var, vars-on-top, no-restricted-globals, regex/invalid, import/extensions */
+// ignore lint error because this file is js as mongoshell
+
+var pagesCollection = db.getCollection('pages');
+var revisionsCollection = db.getCollection('revisions');
+
+var getProcessorArray = require('./processor.js');
+
+var migrationType = process.env.MIGRATION_TYPE;
+var processors = getProcessorArray(migrationType);
+
+var operations = [];
+
+var batchSize = process.env.BATCH_SIZE ?? 100; // default 100 revisions in 1 bulkwrite
+var batchSizeInterval = process.env.BATCH_INTERVAL ?? 3000; // default 3 sec
+
+// ===========================================
+// replace method with processors
+// ===========================================
+function replaceLatestRevisions(body, processors) {
+  var replacedBody = body;
+  processors.forEach((processor) => {
+    replacedBody = processor(replacedBody);
+  });
+  return replacedBody;
+}
+
+if (processors.length === 0) {
+  throw Error('No valid processors found. Please enter a valid environment variable');
+}
+
+pagesCollection.find({}).forEach((doc) => {
+  if (doc.revision) {
+    var revision = revisionsCollection.findOne({ _id: doc.revision });
+    var replacedBody = replaceLatestRevisions(revision.body, [...processors]);
+    var operation = {
+      updateOne: {
+        filter: { _id: revision._id },
+        update: {
+          $set: { body: replacedBody },
+        },
+      },
+    };
+    operations.push(operation);
+
+    // bulkWrite per 100 revisions
+    if (operations.length > (batchSize - 1)) {
+      revisionsCollection.bulkWrite(operations);
+      // sleep time can be set from env var
+      sleep(batchSizeInterval);
+      operations = [];
+    }
+  }
+});
+revisionsCollection.bulkWrite(operations);
+print('migration complete!');

+ 75 - 0
bin/data-migrations/v6/src/processor.js

@@ -0,0 +1,75 @@
+
+/* eslint-disable no-undef, no-var, vars-on-top, no-restricted-globals, regex/invalid */
+// ignore lint error because this file is js as mongoshell
+
+// ===========================================
+// processors for old format
+// ===========================================
+function drawioProcessor(body) {
+  var oldDrawioRegExp = /:::\s?drawio\n(.+?)\n:::/g; // drawio old format
+  return body.replace(oldDrawioRegExp, '``` drawio\n$1\n```');
+}
+
+function plantumlProcessor(body) {
+  var oldPlantUmlRegExp = /@startuml\n([\s\S]*?)\n@enduml/g; // plantUML old format
+  return body.replace(oldPlantUmlRegExp, '``` plantuml\n$1\n```');
+}
+
+function tsvProcessor(body) {
+  var oldTsvTableRegExp = /::: tsv(-h)?\n([\s\S]*?)\n:::/g; // TSV old format
+  return body.replace(oldTsvTableRegExp, '``` tsv$1\n$2\n```');
+}
+
+function csvProcessor(body) {
+  var oldCsvTableRegExp = /::: csv(-h)?\n([\s\S]*?)\n:::/g; // CSV old format
+  return body.replace(oldCsvTableRegExp, '``` csv$1\n$2\n```');
+}
+
+function bracketlinkProcessor(body) {
+  // https://regex101.com/r/btZ4hc/1
+  var oldBracketLinkRegExp = /(?<!\[)\[{1}(\/.*?)\]{1}(?!\])/g; // Page Link old format
+  return body.replace(oldBracketLinkRegExp, '[[$1]]');
+}
+
+// processor for MIGRATION_TYPE=custom
+function customProcessor(body) {
+  // ADD YOUR PROCESS HERE!
+  // https://github.com/weseek/growi/discussions/7180
+  return body;
+}
+
+// ===========================================
+// define processors
+// ===========================================
+
+function getProcessorArray(migrationType) {
+  var oldFormatProcessors;
+  switch (migrationType) {
+    case 'v6-drawio':
+      oldFormatProcessors = [drawioProcessor];
+      break;
+    case 'v6-plantuml':
+      oldFormatProcessors = [plantumlProcessor];
+      break;
+    case 'v6-tsv':
+      oldFormatProcessors = [tsvProcessor];
+      break;
+    case 'v6-csv':
+      oldFormatProcessors = [csvProcessor];
+      break;
+    case 'v6-bracketlink':
+      oldFormatProcessors = [bracketlinkProcessor];
+      break;
+    case 'v6':
+      oldFormatProcessors = [drawioProcessor, plantumlProcessor, tsvProcessor, csvProcessor, bracketlinkProcessor];
+      break;
+    case 'custom':
+      oldFormatProcessors = [customProcessor];
+      break;
+    default:
+      oldFormatProcessors = [];
+  }
+  return oldFormatProcessors;
+}
+
+module.exports = getProcessorArray;

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "6.0.4-RC.0",
+  "version": "6.0.7-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.4-RC.0",
+  "version": "6.0.7-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 2 - 1
packages/app/config/next-i18next.config.ts

@@ -7,8 +7,9 @@ import I18NextLocalStorageBackend from 'i18next-localstorage-backend';
 
 const isDev = process.env.NODE_ENV === 'development';
 
+export const defaultLang = Lang.en_US;
 export const i18n = {
-  defaultLocale: Lang.en_US,
+  defaultLocale: defaultLang,
   locales: AllLang,
 };
 export const defaultNS = 'translation';

+ 1 - 0
packages/app/docker/Dockerfile

@@ -99,6 +99,7 @@ COPY packages/codemirror-textlint packages/codemirror-textlint
 COPY packages/preset-themes packages/preset-themes
 COPY packages/slack packages/slack
 COPY packages/hackmd packages/hackmd
+COPY packages/presentation packages/presentation
 COPY packages/remark-drawio packages/remark-drawio
 COPY packages/remark-growi-directive packages/remark-growi-directive
 COPY packages/remark-lsx packages/remark-lsx

+ 1 - 1
packages/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.0.3`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.3/packages/app/docker/Dockerfile)
+* [`6.0.6`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.6/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 16 - 16
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.4-RC.0",
+  "version": "6.0.7-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -55,7 +55,8 @@
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "next": "/Sandbox rendering is crashed with v12.3 or above ",
     "string-width": "5.0.0 or above exports only ESM.",
-    "prom-client": "!!DO NOT REMOVE!! A peer dependency of @promster."
+    "prom-client": "!!DO NOT REMOVE!! A peer dependency of @promster.",
+    "remark-wiki-link": "!!DO NOT REMOVE!! including 'mdast-util-wiki-link' and 'micromark-extension-wiki-link' required by pukiwiki-like-linker"
   },
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
@@ -66,14 +67,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.4-RC.0",
-    "@growi/core": "^6.0.4-RC.0",
-    "@growi/hackmd": "^6.0.4-RC.0",
-    "@growi/preset-themes": "^6.0.4-RC.0",
-    "@growi/remark-drawio": "^6.0.4-RC.0",
-    "@growi/remark-growi-directive": "^6.0.4-RC.0",
-    "@growi/remark-lsx": "^6.0.4-RC.0",
-    "@growi/slack": "^6.0.4-RC.0",
+    "@growi/codemirror-textlint": "^6.0.7-RC.0",
+    "@growi/core": "^6.0.7-RC.0",
+    "@growi/hackmd": "^6.0.7-RC.0",
+    "@growi/preset-themes": "^6.0.7-RC.0",
+    "@growi/remark-drawio": "^6.0.7-RC.0",
+    "@growi/remark-growi-directive": "^6.0.7-RC.0",
+    "@growi/remark-lsx": "^6.0.7-RC.0",
+    "@growi/slack": "^6.0.7-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -96,8 +97,8 @@
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.1.0",
-    "dayjs": "^1.11.7",
     "date-fns": "^2.23.0",
+    "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
@@ -185,7 +186,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^1.3.0",
+    "swr": "^2.0.3",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
@@ -203,7 +204,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.4-RC.0",
+    "@growi/presentation": "^6.0.7-RC.0",
+    "@growi/ui": "^6.0.7-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
@@ -237,14 +239,12 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-hotkeys": "^2.0.0",
-    "react-waypoint": "^10.1.0",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
-    "reveal.js": "^4.3.1",
     "sass": "^1.53.0",
-    "simplebar-react": "^2.3.6",
     "simple-line-icons": "^2.5.5",
     "simple-load-script": "^1.0.2",
+    "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",

+ 4 - 16
packages/app/public/static/locales/en_US/admin.json

@@ -383,6 +383,7 @@
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
+    "s3_secret_access_key_input_description": "Setting value is hidden",
     "load_plugins": "Load plugins",
     "enable": "Enable",
     "disable": "Disable",
@@ -417,19 +418,6 @@
       "disallow_indent_change": "Disallow change of indent size by users",
       "disallow_indent_change_desc": "Force users to use ther default indent size."
     },
-    "presentation_header": "Presentation setting",
-    "presentation_desc": "You can change presentation settings.",
-    "presentation_options": {
-      "page_break_setting": "Page break setting",
-      "preset_one_separator": "Preset 1",
-      "preset_one_separator_desc": "3 Blank lines",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "Preset 2",
-      "preset_two_separator_desc": "5 Hyphens",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "Custom",
-      "custom_separator_desc": "Regular expression"
-    },
     "xss_header": "Prevent XSS(cross site scripting) setting",
     "xss_desc": "You can change the handling of HTML tags in markdown text.",
     "xss_options": {
@@ -763,9 +751,9 @@
     },
     "reset_password": "Reset Password",
     "reset_password_modal": {
-      "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
-      "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.",
+      "reset_password_info": "When a password is reset, a newly password is sent to the target user.",
+      "password_reset_message": "The temporary password was sent to the below user and strongly recommend to change another one immediately.",
+      "reset_password_alert": "If the e-mail transmission fails, please make sure that e-mail settings are correct and reset password again.",
       "target_user": "Target User",
       "new_password": "New Password"
     },

+ 1 - 1
packages/app/public/static/locales/en_US/commons.json

@@ -123,7 +123,7 @@
 
   "g2g_data_transfer": {
     "tab": "Data transfer",
-    "data_transfer": "GROWI To GROWI Data Transfer",
+    "data_transfer": "Data Transfer",
     "transfer_data_to_this_growi": "Transfer data from another GROWI to this GROWI",
     "publish_transfer_key": "Publish transfer key",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",

+ 1 - 2
packages/app/public/static/locales/en_US/translation.json

@@ -60,7 +60,6 @@
   "attachment_data": "Attachment Data",
   "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "Presentation",
-  "The end": "The end",
   "Not available for guest": "Not available for guest",
   "Not available in this version": "Not available in this version",
   "No users have liked this yet": "No users have liked this yet",
@@ -333,7 +332,7 @@
     "notice": {
       "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."
     },
-    "changes_not_saved": "Changes you made may not be saved."
+    "changes_not_saved": "Changes you made may not be saved. Are you sure you want to move?"
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

+ 4 - 16
packages/app/public/static/locales/ja_JP/admin.json

@@ -391,6 +391,7 @@
     "bucket_name": "バケット名",
     "custom_endpoint": "カスタムエンドポイント",
     "custom_endpoint_change": "MinIOなど、S3互換APIを持つ他のオブジェクトストレージサービスを使用する場合のみ、そのエンドポイントのURLを入力してください。空欄の場合は、Amazon S3を使用します。",
+    "s3_secret_access_key_input_description": "設定値は非表示です",
     "load_plugins": "プラグインを読み込む",
     "enable": "有効",
     "disable": "無効",
@@ -425,19 +426,6 @@
       "disallow_indent_change": "ユーザによるインデント幅変更を許可しない",
       "disallow_indent_change_desc": "ユーザにデフォルトのインデント幅の使用を強制します。"
     },
-    "presentation_header": "プレゼンテーション設定",
-    "presentation_desc": "プレゼンテーションの設定を変更できます。",
-    "presentation_options": {
-      "page_break_setting": "改頁を設定する",
-      "preset_one_separator": "プリセット 1",
-      "preset_one_separator_desc": "連続した空行3行で改頁します",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "プリセット 2",
-      "preset_two_separator_desc": "連続したハイフン5つで改頁します",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "カスタム",
-      "custom_separator_desc": "正規表現を設定できます"
-    },
     "xss_header": "XSS(Cross Site Scripting)対策設定",
     "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "xss_options": {
@@ -771,9 +759,9 @@
     },
     "reset_password": "パスワードのリセット",
     "reset_password_modal": {
-      "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
-      "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
-      "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
+      "reset_password_info": "パスワードをリセットすると新規発行したパスワードを対象ユーザーに送信します。",
+      "password_reset_message": "対象ユーザーに一時的なパスワードを送信しました。新しく別のパスワードを設定するよう伝えてください。",
+      "reset_password_alert": "送信に失敗した場合はメール設定が正しいことを確認し再度パスワードのリセットを行ってください",
       "target_user": "対象ユーザー",
       "new_password": "新しいパスワード"
     },

+ 2 - 2
packages/app/public/static/locales/ja_JP/commons.json

@@ -122,11 +122,11 @@
 
   "g2g_data_transfer": {
     "tab": "データ移行",
-    "data_transfer": "別GROWIとのデータ移行",
+    "data_transfer": "データ移行",
     "transfer_data_to_this_growi": "別GROWIのデータをこのGROWIへ移行する",
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
-    "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ移行はご利用いただけなくなります。",
+    "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
     "transfer_to_growi_cloud": "※ GROWI.cloud への移行を実施する場合はこちらをご確認ください。"
   }
 }

+ 1 - 2
packages/app/public/static/locales/ja_JP/translation.json

@@ -57,7 +57,6 @@
   "attachment_data": "添付データ",
   "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "プレゼンテーション",
-  "The end": "おしまい",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
   "No users have liked this yet": "いいねをしているユーザーはいません",
@@ -332,7 +331,7 @@
     "notice": {
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     },
-    "changes_not_saved": "変更が保存されていない可能性があります。"
+    "changes_not_saved": "変更が保存されていない可能性があります。本当に移動しますか?"
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",

+ 4 - 16
packages/app/public/static/locales/zh_CN/admin.json

@@ -391,6 +391,7 @@
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "输入对象存储服务(如MinIO)端点的URL,MinIO具有与S3兼容的API。如果为空,则使用Amazon S3。",
+    "s3_secret_access_key_input_description": "设定的值被隐藏。",
     "load_plugins": "加载插件",
     "enable": "启用",
     "disable": "停用",
@@ -425,19 +426,6 @@
       "disallow_indent_change": "不允许用户更改缩进值",
       "disallow_indent_change_desc": "您可以不允许用户更改缩进值。"
     },
-    "presentation_header": "演示文稿设置",
-    "presentation_desc": "您可以更改演示文稿设置。",
-    "presentation_options": {
-      "page_break_setting": "分页设置",
-      "preset_one_separator": "预设 1",
-      "preset_one_separator_desc": "3 空行",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "预设 2",
-      "preset_two_separator_desc": "5 连字符",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "自定义",
-      "custom_separator_desc": "正则表达式"
-    },
     "xss_header": "阻止XSS(跨站点脚本)设置",
     "xss_desc": "您可以更改标记文本中HTML标记的处理方式。",
     "xss_options": {
@@ -771,9 +759,9 @@
     },
     "reset_password": "重置密码",
     "reset_password_modal": {
-      "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
-      "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.",
+      "reset_password_info": "When a password is reset, a newly password is sent to the target user.",
+      "password_reset_message": "The temporary password was sent to the below user and strongly recommend to change another one immediately.",
+      "reset_password_alert": "If the e-mail transmission fails, please make sure that e-mail settings are correct and reset password again.",
       "target_user": "Target User",
       "new_password": "New Password"
     },

+ 1 - 1
packages/app/public/static/locales/zh_CN/commons.json

@@ -123,7 +123,7 @@
 
   "g2g_data_transfer": {
     "tab": "数据迁移",
-    "data_transfer": "与另一个GROWI的数据转移",
+    "data_transfer": "数据迁移",
     "transfer_data_to_this_growi": "将数据从另一个GROWI迁移到这个GROWI上",
     "publish_transfer_key": "发布迁移密钥",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",

+ 1 - 2
packages/app/public/static/locales/zh_CN/translation.json

@@ -57,7 +57,6 @@
   "attachment_data": "Attachment Data",
   "No_attachments_yet": "暂无附件",
 	"Presentation Mode": "演示文稿",
-  "The end": "结束",
   "Not available for guest": "不提供给客人",
   "Not available in this version": "此版本中不提供",
   "No users have liked this yet": "还没有用户喜欢这个",
@@ -322,7 +321,7 @@
 		"notice": {
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 		},
-    "changes_not_saved": "您所做的更改可能不会保存。"
+    "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

+ 6 - 6
packages/app/resource/locales/en_US/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 11 - 0
packages/app/resource/locales/en_US/admin/userResetPassword.txt

@@ -0,0 +1,11 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>

+ 8 - 8
packages/app/resource/locales/en_US/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 ====
 Created user:
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 

+ 3 - 3
packages/app/resource/locales/en_US/notifications/comment.txt

@@ -1,9 +1,9 @@
-<%- username }} commented on {{ path -%>.
+<%- username %> commented on <%- path %>.
 
 ----------------------
 
-<%- comment -%>
+<%- comment %>
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 Password Reset
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
-A request has been received to change the password from <%- appTitle -%>.
+A request has been received to change the password from <%- appTitle %>.
 However, this email is not registerd. Please try again with different email.
 
 If you did not request a password reset, you can safely ignore this email.
 
 -------------------------------------------------------------------------
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-<%- username -%> created a new page under <%- path -%>.
+<%- username %> created a new page under <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-<%- username -%> deleted the page  <%- path -%>.
+<%- username %> deleted the page  <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-<%- username -%> edited the page <%- path -%>.
+<%- username %> edited the page <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-<%- username -%> liked the page <%- path -%>.
+<%- username %> liked the page <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
+<%- username %> renamed the page <%- oldPath %> to <%- newPath %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 Password Reset
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
-A request has been received to change the password your GROWI (<%- appTitle -%>) account.
+A request has been received to change the password your GROWI (<%- appTitle %>) account.
 To reset your password, click on the link below.
 
-<%- url -%>
+<%- url %>
 
-This link will expire in 10 minutes at  <%- expiredAt -%>.
+This link will expire in 10 minutes at  <%- expiredAt %>.
 
 If you did not request a password reset, you can safely ignore this email.

+ 1 - 1
packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 Password Reset Successful
 
-Hi <%- email -%>
+Hi <%- email %>
 
 Your password has been successfully reset.
 Please log in with your new password.

+ 4 - 4
packages/app/resource/locales/en_US/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 Account confirmation
 
-Hi, <%- email -%>
+Hi, <%- email %>
 
-An acount has been created in GROWI (<%- appTitle -%>).
+An acount has been created in GROWI (<%- appTitle %>).
 To activate your account, click on the link below.
 
-<%- url -%>
+<%- url %>
 
-This link will expire in 1 hour at  <%- expiredAt -%>.
+This link will expire in 1 hour at  <%- expiredAt %>.
 
 If you did not created the account, you can safely ignore this email.

+ 6 - 6
packages/app/resource/locales/ja_JP/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 12 - 0
packages/app/resource/locales/ja_JP/admin/userResetPassword.txt

@@ -0,0 +1,12 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>
+

+ 8 - 8
packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 ====
 Created user:
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 パスワードリセット
 
-こんにちは、 <%- email -%>
+こんにちは、 <%- email %>
 
-<%- appTitle -%> からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
+<%- appTitle %> からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
 他のemailアドレスで再度お試しください。
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。
 
 -------------------------------------------------------------------------
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 パスワード リセット
 
-こんにちは, <%- email -%>
+こんにちは, <%- email %>
 
-あなたのGROWI (<%- appTitle -%>) アカウントから、パスワード再設定のリクエストがありました。
+あなたのGROWI (<%- appTitle %>) アカウントから、パスワード再設定のリクエストがありました。
 パスワードをリセットするには、以下のリンクをクリックしてください。
 
-<%- url -%>
+<%- url %>
 
-このリンクは10分後の <%- expiredAt -%> に失効します。
+このリンクは10分後の <%- expiredAt %> に失効します。
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。

+ 1 - 1
packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 パスワードリセットに成功
 
-こんにちは、 <%- email -%>
+こんにちは、 <%- email %>
 
 あなたのパスワードは正常にリセットされました。
 新しいパスワードでログインしてください。

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/userActivation.txt

@@ -1,13 +1,13 @@
 仮登録完了のお知らせ
 
-<%- email -%> さん
+<%- email %> さん
 
-GROWI (<%- appTitle -%>) で仮登録が完了いたしました。
+GROWI (<%- appTitle %>) で仮登録が完了いたしました。
 
 ご本人様確認のため、下記リンクをクリックし、アカウントの本登録を完了させて下さい。
 
-<%- url -%>
+<%- url %>
 
-このリンクは1時間後の <%- expiredAt -%> に失効します。
+このリンクは1時間後の <%- expiredAt %> に失効します。
 
 ※当メールの内容に心当たりがない場合は、このメールを無視してください。

+ 6 - 6
packages/app/resource/locales/zh_CN/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, <%- email -%>
+Hi, <%- email %>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: <%- email -%>
-Password: <%- password -%>
+Email: <%- email %>
+Password: <%- password %>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-<%- url -%>
+<%- url %>
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>

+ 11 - 0
packages/app/resource/locales/zh_CN/admin/userResetPassword.txt

@@ -0,0 +1,11 @@
+Hi, <%- email %>
+
+Your password has been reset by the administrator, you can log in with following account:
+
+Email: <%- email %>
+New Password: <%- password %>
+(This password was auto generated. Update required at the first time you logging in)
+
+--
+<%- appTitle %>
+<%- url %>

+ 8 - 8
packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, <%- adminUser.name -%>
+Hi, <%- adminUser.name %>
 
-A user registered to <%- appTitle -%>.
+A user registered to <%- appTitle %>.
 
 
 ====
 Created user:
 
-Name: <%- createdUser.name -%>
-User Name: <%- createdUser.username -%>
-Email: <%- createdUser.email -%>
+Name: <%- createdUser.name %>
+User Name: <%- createdUser.username %>
+Email: <%- createdUser.email %>
 ====
 
 Please do some action with following URL:
-<%- url -%>/admin/users
+<%- url %>/admin/users
 
 
 --
-<%- appTitle -%>
-<%- url -%>
+<%- appTitle %>
+<%- url %>
 

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/comment.txt

@@ -1,9 +1,9 @@
-<%- username -%> commented on <%- path -%>.
+<%- username %> commented on <%- path %>.
 
 ----------------------
 
-<%- comment -%>
+<%- comment %>
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt

@@ -1,13 +1,13 @@
 重设密码
 
-嗨,<%-电子邮件-%>
+嗨,<%-电子邮件%>
 
-已收到来自 <%-appTitle-%> 的更改密码请求。
+已收到来自 <%-appTitle%> 的更改密码请求。
 但是,此电子邮件未注册。请使用其他电子邮件重试。
 
 如果您没有要求重置密码,则可以放心地忽略此电子邮件。
 
 -------------------------------------------------------------------------
 
-GROWI: <%- appTitle -%>
-URL: <%- url -%>
+GROWI: <%- appTitle %>
+URL: <%- url %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-<%- username -%> created a new page under <%- path -%>.
+<%- username %> created a new page under <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-<%- username -%> deleted the page  <%- path -%>.
+<%- username %> deleted the page  <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-<%- username -%> edited the page <%- path -%>.
+<%- username %> edited the page <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-<%- username -%> liked the page <%- path -%>.
+<%- username %> liked the page <%- path %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
+<%- username %> renamed the page <%- oldPath %> to <%- newPath %>.
 
 ----------------------
 
-Growi: <%- appTitle -%>
+Growi: <%- appTitle %>

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/passwordReset.txt

@@ -1,11 +1,11 @@
 重设密码
 
-嗨,<%- email -%>
+嗨,<%- email %>
 
-已收到更改您 GROWI (<%-appTitle-%>) 帐户 密码的请求。
+已收到更改您 GROWI (<%-appTitle%>) 帐户 密码的请求。
 要重置密码,请单击下面的链接。
 
-<%- url -%>
+<%- url %>
 
 这个链接在10分钟后的{ expiredAt }}失效。
 

+ 1 - 1
packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 密码重置成功
 
-嗨, <%-email-%>
+嗨, <%-email%>
 
 您的密码已成功重置。
 请使用您的新密码登录。

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 确认账户创建
 
-致<%- email -%>,
+致<%- email %>,
 
-已使用 GROWI (<%- appTitle -%>) 创建帐户。
+已使用 GROWI (<%- appTitle %>) 创建帐户。
 单击下面的链接以激活您的帐户。
 
-<%- url -%>
+<%- url %>
 
-这个链接将在1小时后即<%- expiredAt -%>失效。
+这个链接将在1小时后即<%- expiredAt %>失效。
 
 如果您尚未创建,请忽略此电子邮件。

+ 0 - 42
packages/app/src/client/legacy/crowi-presentation.js

@@ -1,42 +0,0 @@
-import Reveal from 'reveal.js';
-
-Reveal.initialize({
-  controls: true,
-  progress: true,
-  history: true,
-  center: true,
-  transition: 'slide',
-
-  // // This specification method can't be used
-  // //   sice deleting symlink prevented `src` from being resolved -- 2017.06.15 Yuki Takei
-  //
-  // Optional libraries used to extend on reveal.js
-  // dependencies: [
-  //   { src: 'lib/js/classList.js', condition: function() { return !document.body.classList; } },
-  //   { src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
-  //   { src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
-  //   { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } },
-  //   { src: 'plugin/zoom-js/zoom.js', async: true, condition: function() { return !!document.body.classList; } },
-  //   { src: 'plugin/notes/notes.js', async: true, condition: function() { return !!document.body.classList; } }
-  // ]
-});
-
-require.ensure([], () => {
-  require('reveal.js/plugin/zoom/zoom');
-  require('reveal.js/plugin/notes/notes');
-  require('../util/reveal/plugins/growi-renderer');
-
-  // fix https://github.com/weseek/crowi-plus/issues/96
-  Reveal.slide(0, 0);
-  Reveal.sync();
-});
-
-Reveal.addEventListener('ready', () => {
-  // event.currentSlide, event.indexh, event.indexv
-  $('.reveal section').each(function() {
-    const $self = $(this);
-    if ($self.children().length !== 1) {
-      $self.addClass('only');
-    }
-  });
-});

+ 0 - 1
packages/app/src/client/services/AdminAppContainer.js

@@ -108,7 +108,6 @@ export default class AdminAppContainer extends Container {
       s3CustomEndpoint: appSettingsParams.s3CustomEndpoint,
       s3Bucket: appSettingsParams.s3Bucket,
       s3AccessKeyId: appSettingsParams.s3AccessKeyId,
-      s3SecretAccessKey: appSettingsParams.s3SecretAccessKey,
       s3ReferenceFileWithRelayMode: appSettingsParams.s3ReferenceFileWithRelayMode,
 
       gcsUseOnlyEnvVars: appSettingsParams.gcsUseOnlyEnvVars,

+ 0 - 35
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -24,8 +24,6 @@ export default class AdminMarkDownContainer extends Container {
       isEnabledLinebreaksInComments: false,
       adminPreferredIndentSize: 4,
       isIndentSizeForced: false,
-      pageBreakSeparator: 1,
-      pageBreakCustomSeparator: '',
       isEnabledXss: false,
       xssOption: '',
       tagWhiteList: '',
@@ -55,8 +53,6 @@ export default class AdminMarkDownContainer extends Container {
       isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
       adminPreferredIndentSize: markdownParams.adminPreferredIndentSize,
       isIndentSizeForced: markdownParams.isIndentSizeForced,
-      pageBreakSeparator: markdownParams.pageBreakSeparator,
-      pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
       isEnabledXss: markdownParams.isEnabledXss,
       xssOption: markdownParams.xssOption,
       tagWhiteList: markdownParams.tagWhiteList || '',
@@ -68,20 +64,6 @@ export default class AdminMarkDownContainer extends Container {
     this.setState({ adminPreferredIndentSize });
   }
 
-  /**
-   * Switch PageBreakSeparator
-   */
-  switchPageBreakSeparator(pageBreakSeparator) {
-    this.setState({ pageBreakSeparator });
-  }
-
-  /**
-   * Set PageBreakCustomSeparator
-   */
-  setPageBreakCustomSeparator(pageBreakCustomSeparator) {
-    this.setState({ pageBreakCustomSeparator });
-  }
-
   /**
    * Switch enableXss
    */
@@ -140,21 +122,4 @@ export default class AdminMarkDownContainer extends Container {
     });
   }
 
-  /**
-   * Update Presentation Setting
-   */
-  async updatePresentationSetting() {
-
-    const response = await apiv3Put('/markdown-setting/presentation', {
-      pageBreakSeparator: this.state.pageBreakSeparator,
-      pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
-    });
-
-    this.setState({
-      pageBreakSeparator: response.data.presentationParams.pageBreakSeparator,
-      pageBreakCustomSeparator: response.data.presentationParams.pageBreakCustomSeparator,
-    });
-    return response;
-  }
-
 }

+ 5 - 11
packages/app/src/client/services/layout.ts

@@ -1,3 +1,4 @@
+import type { IPage } from '~/interfaces/page';
 import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';
@@ -5,25 +6,18 @@ import { useEditorMode } from '~/stores/ui';
 export const useEditorModeClassName = (): string => {
   const { getClassNamesByEditorMode } = useEditorMode();
 
-  // TODO: Enable `editing-sidebar` class somehow
-  // https://redmine.weseek.co.jp/issues/111527
-  // const classNames: string[] = [];
-  // if (currentPage != null) {
-  //   const isSidebar = currentPage.path === '/Sidebar';
-  //   classNames.push(...getClassNamesByEditorMode(/* isSidebar */));
-  // }
-
   return `${getClassNamesByEditorMode().join(' ') ?? ''}`;
 };
 
-export const useCurrentGrowiLayoutFluidClassName = (): string => {
+export const useCurrentGrowiLayoutFluidClassName = (initialPage?: IPage): string => {
   const { data: currentPage } = useSWRxCurrentPage();
 
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 
-  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
+  const page = currentPage ?? initialPage;
+  const isContainerFluidEachPage = page == null || !('expandContentWidth' in page)
     ? null
-    : currentPage.expandContentWidth;
+    : page.expandContentWidth;
   const isContainerFluidDefault = dataIsContainerFluid;
   const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
 

+ 2 - 2
packages/app/src/client/services/page-operation.ts

@@ -6,7 +6,7 @@ import urljoin from 'url-join';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
@@ -179,7 +179,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
 
 export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);

+ 89 - 0
packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -0,0 +1,89 @@
+import { useCallback, useEffect } from 'react';
+
+import EventEmitter from 'events';
+
+import { DrawioEditByViewerProps } from '@growi/remark-drawio';
+
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import mdu from '~/components/PageEditor/MarkdownDrawioUtil';
+import type { OptionsToSave } from '~/interfaces/page-operation';
+import { useShareLinkId } from '~/stores/context';
+import { useDrawioModal } from '~/stores/modal';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var globalEmitter: EventEmitter;
+}
+
+
+export const useDrawioModalLauncherForView = (opts?: {
+  onSaveSuccess?: () => void,
+  onSaveError?: (error: any) => void,
+}): void => {
+
+  const { data: shareLinkId } = useShareLinkId();
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
+
+  const { open: openDrawioModal } = useDrawioModal();
+
+  const saveOrUpdate = useSaveOrUpdate();
+
+  const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
+    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+      return;
+    }
+
+    const currentMarkdown = currentPage.revision.body;
+    const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
+
+    const optionsToSave: OptionsToSave = {
+      isSlackEnabled: false,
+      slackChannels: '',
+      grant: currentPage.grant,
+      grantUserGroupId: currentPage.grantedGroup?._id,
+      grantUserGroupName: currentPage.grantedGroup?.name,
+      pageTags: tagsInfo.tags,
+    };
+
+    try {
+      const currentRevisionId = currentPage.revision._id;
+      await saveOrUpdate(
+        newMarkdown,
+        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
+        optionsToSave,
+      );
+
+      opts?.onSaveSuccess?.();
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      opts?.onSaveError?.(error);
+    }
+  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+
+
+  // set handler to open DrawioModal
+  useEffect(() => {
+    // disable if share link
+    if (shareLinkId != null) {
+      return;
+    }
+
+    const handler = (data: DrawioEditByViewerProps) => {
+      openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
+    };
+    globalEmitter.on('launchDrawioModal', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('launchDrawioModal', handler);
+    };
+  }, [openDrawioModal, saveByDrawioModal, shareLinkId]);
+};

+ 33 - 0
packages/app/src/client/services/side-effects/hackmd-draft-updated.ts

@@ -0,0 +1,33 @@
+import { useCallback, useEffect } from 'react';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageId } from '~/stores/context';
+import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
+import { useGlobalSocket } from '~/stores/websocket';
+
+export const useHackmdDraftUpdatedEffect = (): void => {
+
+  const { data: currentPageId } = useCurrentPageId();
+  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
+
+  const { data: socket } = useGlobalSocket();
+
+  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+    if (s2cMessagePageUpdated.pageId === currentPageId) {
+      mutateIsHackmdDraftUpdatingInRealtime(true);
+    }
+  }, [currentPageId, mutateIsHackmdDraftUpdatingInRealtime]);
+
+  // listen socket for hackmd saved
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+
+    return () => {
+      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+    };
+  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
+};

+ 89 - 0
packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -0,0 +1,89 @@
+import { useCallback, useEffect } from 'react';
+
+import EventEmitter from 'events';
+
+import MarkdownTable from '~/client/models/MarkdownTable';
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import mtu from '~/components/PageEditor/MarkdownTableUtil';
+import type { OptionsToSave } from '~/interfaces/page-operation';
+import { useShareLinkId } from '~/stores/context';
+import { useHandsontableModal } from '~/stores/modal';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var globalEmitter: EventEmitter;
+}
+
+
+export const useHandsontableModalLauncherForView = (opts?: {
+  onSaveSuccess?: () => void,
+  onSaveError?: (error: any) => void,
+}): void => {
+
+  const { data: shareLinkId } = useShareLinkId();
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
+
+  const { open: openHandsontableModal } = useHandsontableModal();
+
+  const saveOrUpdate = useSaveOrUpdate();
+
+  const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
+    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+      return;
+    }
+
+    const currentMarkdown = currentPage.revision.body;
+    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
+
+    const optionsToSave: OptionsToSave = {
+      isSlackEnabled: false,
+      slackChannels: '',
+      grant: currentPage.grant,
+      grantUserGroupId: currentPage.grantedGroup?._id,
+      grantUserGroupName: currentPage.grantedGroup?.name,
+      pageTags: tagsInfo.tags,
+    };
+
+    try {
+      const currentRevisionId = currentPage.revision._id;
+      await saveOrUpdate(
+        newMarkdown,
+        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
+        optionsToSave,
+      );
+
+      opts?.onSaveSuccess?.();
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      opts?.onSaveError?.(error);
+    }
+  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+
+
+  // set handler to open HandsonTableModal
+  useEffect(() => {
+    if (currentPage == null || shareLinkId != null) {
+      return;
+    }
+
+    const handler = (bol: number, eol: number) => {
+      const markdown = currentPage.revision.body;
+      const currentMarkdownTable = mtu.getMarkdownTableFromLine(markdown, bol, eol);
+      openHandsontableModal(currentMarkdownTable, undefined, false, table => saveByHandsontableModal(table, bol, eol));
+    };
+    globalEmitter.on('launchHandsonTableModal', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('launchHandsonTableModal', handler);
+    };
+  }, [currentPage, openHandsontableModal, saveByHandsontableModal, shareLinkId]);
+};

+ 3 - 6
packages/app/src/components/EventListeneres/HashChanged.tsx → packages/app/src/client/services/side-effects/hash-changed.ts

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
 
 import { useRouter } from 'next/router';
 
@@ -8,8 +8,9 @@ import { useEditorMode, determineEditorModeByHash } from '~/stores/ui';
 /**
  * Change editorMode by browser forward/back operation
  */
-const HashChanged = (): JSX.Element => {
+export const useHashChangedEffect = (): void => {
   const router = useRouter();
+
   const { data: isEditable } = useIsEditable();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
 
@@ -47,8 +48,4 @@ const HashChanged = (): JSX.Element => {
       router.events.off('routeChangeComplete', hashchangeHandler);
     };
   }, [hashchangeHandler, router.events]);
-
-  return <></>;
 };
-
-export default HashChanged;

+ 39 - 0
packages/app/src/client/services/side-effects/page-updated.ts

@@ -0,0 +1,39 @@
+import { useCallback, useEffect } from 'react';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import { useGlobalSocket } from '~/stores/websocket';
+
+export const usePageUpdatedEffect = (): void => {
+
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+
+  const { data: socket } = useGlobalSocket();
+
+  const setLatestRemotePageData = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+
+    const remoteData = {
+      remoteRevisionId: s2cMessagePageUpdated.revisionId,
+      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
+      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
+      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
+      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
+    };
+    setRemoteLatestPageData(remoteData);
+  }, [setRemoteLatestPageData]);
+
+  // listen socket for someone updating this page
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
+
+    return () => {
+      socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
+    };
+
+  }, [setLatestRemotePageData, socket]);
+};

+ 1 - 6
packages/app/src/client/services/user-ui-settings.ts

@@ -1,11 +1,9 @@
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
-
 import { debounce } from 'throttle-debounce';
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { useIsGuestUser } from '~/stores/context';
 
 let settingsForBulk: Partial<IUserUISettings> = {};
 const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
@@ -33,11 +31,8 @@ type UserUISettingsUtil = {
   scheduleToPut: ScheduleToPutFunction | (() => void),
 }
 export const useUserUISettings = (): UserUISettingsUtil => {
-  const { data: isGuestUser } = useIsGuestUser();
 
   return {
-    scheduleToPut: isGuestUser
-      ? () => {}
-      : scheduleToPut,
+    scheduleToPut,
   };
 };

+ 60 - 0
packages/app/src/client/util/locale-utils.ts

@@ -1,9 +1,69 @@
+import type { IncomingHttpHeaders } from 'http';
+
+import { Lang } from '@growi/core';
+
+import * as nextI18NextConfig from '^/config/next-i18next.config';
+
 // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
 const DIAGRAMS_NET_LANG_MAP = {
   ja_JP: 'ja',
   zh_CN: 'zh',
 };
 
+const ACCEPT_LANG_MAP = {
+  en: Lang.en_US,
+  ja: Lang.ja_JP,
+  zh: Lang.zh_CN,
+};
+
 export const getDiagramsNetLangCode = (lang) => {
   return DIAGRAMS_NET_LANG_MAP[lang];
 };
+
+/**
+ * It return the first language that matches ACCEPT_LANG_MAP keys from sorted accept languages array
+ * @param sortedAcceptLanguagesArray
+ */
+const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
+  for (const lang of sortedAcceptLanguagesArray) {
+    const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key));
+    if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
+  }
+  return nextI18NextConfig.defaultLang;
+};
+
+/**
+  * Detect locale from browser accept language
+  * @param headers
+  */
+export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeaders): Lang => {
+  // 1. get the header accept-language
+  // ex. "ja,ar-SA;q=0.8,en;q=0.6,en-CA;q=0.4,en-US;q=0.2"
+  const acceptLanguages = headers['accept-language'];
+
+  if (acceptLanguages == null) {
+    return nextI18NextConfig.defaultLang;
+  }
+
+  // 1. trim blank spaces.
+  // 2. separate by ,.
+  // 3. if "lang;q=x", then { 'x', 'lang' } to add to the associative array.
+  //    if "lang" has no weight x (";q=x"), add it with key = 1.
+  // ex. {'1': 'ja','0.8': 'ar-SA','0.6': 'en','0.4': 'en-CA','0.2': 'en-US'}
+  const acceptLanguagesDict = acceptLanguages
+    .replace(/\s+/g, '')
+    .split(',')
+    .map(item => item.split(/\s*;\s*q\s*=\s*/))
+    .reduce((acc, [key, value = '1']) => {
+      acc[value] = key;
+      return acc;
+    }, {});
+
+  // 1. create an array of sorted languages in descending order.
+  // ex. [ 'ja', 'ar-SA', 'en', 'en-CA', 'en-US' ]
+  const sortedAcceptLanguagesArray = Object.keys(acceptLanguagesDict)
+    .sort((x, y) => y.localeCompare(x))
+    .map(item => acceptLanguagesDict[item]);
+
+  return getPreferredLanguage(sortedAcceptLanguagesArray);
+};

+ 0 - 107
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -1,107 +0,0 @@
-/**
- * reveal.js growi-renderer plugin.
- */
-(function(root, factory) {
-  const growiRendererPlugin = factory();
-  growiRendererPlugin.initialize();
-}(this, () => {
-  /* eslint-disable no-useless-escape */
-  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
-  const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
-  const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
-  /* eslint-enable no-useless-escape */
-
-  const growiRenderer = window.parent.previewRenderer;
-
-  let marked;
-
-  /**
-   * Add data separator before lines
-   * starting with '#' to markdown.
-   */
-  function divideSlides() {
-    const sections = document.querySelectorAll('[data-markdown]');
-    for (let i = 0, len = sections.length; i < len; i++) {
-      const section = sections[i];
-      const markdown = marked.getMarkdownFromSlide(section);
-      const context = { markdown };
-      const { interceptorManager } = window.parent;
-      let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
-      // replace string '\n' to LF code.
-      dataSeparator = dataSeparator.replace(/\\n/g, '\n');
-      const replaceValue = `${dataSeparator}#`;
-      // detach code block.
-      interceptorManager.process('prePreProcess', context);
-      // if there is only '\n' in the first line, replace it.
-      context.markdown = context.markdown.replace(/^\n/, '');
-      // add data separator to markdown.
-      context.markdown = context.markdown.replace(/[\n]+#/g, replaceValue);
-      // restore code block.
-      interceptorManager.process('postPreProcess', context);
-      section.innerHTML = marked.createMarkdownSlide(context.markdown);
-    }
-  }
-
-  /**
-   * Converts data-markdown slides to HTML slides by GrowiRenderer.
-   */
-  function convertSlides() {
-    const sections = document.querySelectorAll('[data-markdown]');
-    let markdown;
-    const { interceptorManager } = window.parent;
-
-    for (let i = 0, len = sections.length; i < len; i++) {
-      const section = sections[i];
-
-      // Only parse the same slide once
-      if (!section.getAttribute('data-markdown-parsed')) {
-        section.setAttribute('data-markdown-parsed', 'true');
-        const notes = section.querySelector('aside.notes');
-        markdown = marked.getMarkdownFromSlide(section);
-        const context = { markdown, currentPathname: decodeURIComponent(window.parent.location.pathname) };
-
-        interceptorManager.process('preRender', context)
-          .then(() => { return interceptorManager.process('prePreProcess', context) })
-          .then(() => {
-            context.markdown = growiRenderer.preProcess(context.markdown, context);
-          })
-          .then(() => { return interceptorManager.process('postPreProcess', context) })
-          .then(() => {
-            context.parsedHTML = growiRenderer.process(context.markdown, context);
-          })
-          .then(() => { return interceptorManager.process('prePostProcess', context) })
-          .then(() => {
-            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-          })
-          .then(() => { return interceptorManager.process('postPostProcess', context) })
-          .then(() => { return interceptorManager.process('preRenderHtml', context) })
-          .then(() => { return interceptorManager.process('postRenderHtml', context) })
-          .then(() => {
-            section.innerHTML = context.parsedHTML;
-          });
-        marked.addAttributes(section, section, null, section.getAttribute('data-element-attributes')
-          || section.parentNode.getAttribute('data-element-attributes')
-          || DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-        section.getAttribute('data-attributes')
-          || section.parentNode.getAttribute('data-attributes')
-          || DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
-
-        // If there were notes, we need to re-add them after
-        // having overwritten the section's HTML
-        if (notes) {
-          section.appendChild(notes);
-        }
-      }
-    }
-  }
-
-  // API
-  return {
-    async initialize() {
-      marked = require('./markdown').default(growiRenderer.process);
-      divideSlides();
-      marked.processSlides();
-      convertSlides();
-    },
-  };
-}));

+ 0 - 379
packages/app/src/client/util/reveal/plugins/markdown.js

@@ -1,379 +0,0 @@
-/**
- * The reveal.js markdown plugin. Handles parsing of
- * markdown inside of presentations as well as loading
- * of external markdown documents.
- * Referred from The reveal.js markdown plugin.
- * https://github.com/hakimel/reveal.js/blob/master/plugin/markdown/markdown.js
- */
-export default function( marked ) {
-
-  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$',
-    DEFAULT_NOTES_SEPARATOR = 'notes?:',
-    DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
-    DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
-
-  const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
-
-
-  /**
-   * Retrieves the markdown contents of a slide section
-   * element. Normalizes leading tabs/whitespace.
-   */
-  function getMarkdownFromSlide( section ) {
-
-    // look for a <script> or <textarea data-template> wrapper
-    let template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
-
-    // strip leading whitespace so it isn't evaluated as code
-    let text = ( template || section ).textContent;
-
-    // restore script end tags
-    text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
-
-    let leadingWs = text.match( /^\n?(\s*)/ )[1].length,
-      leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
-
-    if ( leadingTabs > 0 ) {
-      text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}', 'g'), '\n' );
-    }
-    else if ( leadingWs > 1 ) {
-      text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' );
-    }
-
-    return text;
-
-  }
-
-  /**
-   * Given a markdown slide section element, this will
-   * return all arguments that aren't related to markdown
-   * parsing. Used to forward any other user-defined arguments
-   * to the output markdown slide.
-   */
-  function getForwardedAttributes( section ) {
-
-    let attributes = section.attributes;
-    let result = [];
-
-    for ( let i = 0, len = attributes.length; i < len; i++ ) {
-      let name = attributes[i].name,
-        value = attributes[i].value;
-
-      // disregard attributes that are used for markdown loading/parsing
-      if ( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
-
-      if ( value ) {
-        result.push( name + '="' + value + '"' );
-      }
-      else {
-        result.push( name );
-      }
-    }
-
-    return result.join( ' ' );
-
-  }
-
-  /**
-   * Inspects the given options and fills out default
-   * values for what's not defined.
-   */
-  function getSlidifyOptions( options ) {
-
-    options = options || {};
-    options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR;
-    options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR;
-    options.attributes = options.attributes || '';
-
-    return options;
-
-  }
-
-  /**
-   * Helper function for constructing a markdown slide.
-   */
-  function createMarkdownSlide( content, options ) {
-
-    options = getSlidifyOptions( options );
-
-    let notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
-
-    if ( notesMatch.length === 2 ) {
-      content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
-    }
-
-    // prevent script end tags in the content from interfering
-    // with parsing
-    content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
-
-    return '<script type="text/template">' + content + '</script>';
-
-  }
-
-  /**
-   * Parses a data string into multiple slides based
-   * on the passed in separator arguments.
-   */
-  function slidify( markdown, options ) {
-
-    options = getSlidifyOptions( options );
-
-    let separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
-      horizontalSeparatorRegex = new RegExp( options.separator );
-
-    let matches,
-      lastIndex = 0,
-      isHorizontal,
-      wasHorizontal = true,
-      content,
-      sectionStack = [];
-
-    // iterate until all blocks between separators are stacked up
-    while ( (matches = separatorRegex.exec( markdown )) != null ) {
-      // notes = null;
-
-      // determine direction (horizontal by default)
-      isHorizontal = horizontalSeparatorRegex.test( matches[0] );
-
-      if ( !isHorizontal && wasHorizontal ) {
-        // create vertical stack
-        sectionStack.push( [] );
-      }
-
-      // pluck slide content from markdown input
-      content = markdown.substring( lastIndex, matches.index );
-
-      if ( isHorizontal && wasHorizontal ) {
-        // add to horizontal stack
-        sectionStack.push( content );
-      }
-      else {
-        // add to vertical stack
-        sectionStack[sectionStack.length-1].push( content );
-      }
-
-      lastIndex = separatorRegex.lastIndex;
-      wasHorizontal = isHorizontal;
-    }
-
-    // add the remaining slide
-    ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
-
-    let markdownSections = '';
-
-    // flatten the hierarchical stack, and insert <section data-markdown> tags
-    for ( let i = 0, len = sectionStack.length; i < len; i++ ) {
-      // vertical
-      if ( sectionStack[i] instanceof Array ) {
-        markdownSections += '<section '+ options.attributes +'>';
-
-        sectionStack[i].forEach( function( child ) {
-          markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
-        } );
-
-        markdownSections += '</section>';
-      }
-      else {
-        markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
-      }
-    }
-
-    return markdownSections;
-
-  }
-
-  /**
-   * Parses any current data-markdown slides, splits
-   * multi-slide markdown into separate sections and
-   * handles loading of external markdown.
-   */
-  function processSlides() {
-
-    let sections = document.querySelectorAll( '[data-markdown]'),
-      section;
-
-    for ( let i = 0, len = sections.length; i < len; i++ ) {
-
-      section = sections[i];
-
-      if ( section.getAttribute( 'data-markdown' ).length ) {
-
-        let xhr = new XMLHttpRequest(),
-          url = section.getAttribute( 'data-markdown' );
-
-        let datacharset = section.getAttribute( 'data-charset' );
-
-        // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
-        if ( datacharset != null && datacharset != '' ) {
-          xhr.overrideMimeType( 'text/html; charset=' + datacharset );
-        }
-
-        xhr.onreadystatechange = function() {
-          if ( xhr.readyState === 4 ) {
-            // file protocol yields status code 0 (useful for local debug, mobile applications etc.)
-            if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
-
-              section.outerHTML = slidify( xhr.responseText, {
-                separator: section.getAttribute( 'data-separator' ),
-                verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-                notesSeparator: section.getAttribute( 'data-separator-notes' ),
-                attributes: getForwardedAttributes( section )
-              });
-
-            }
-            else {
-
-              section.outerHTML = '<section data-state="alert">' +
-                'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
-                'Check your browser\'s JavaScript console for more details.' +
-                '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
-                '</section>';
-
-            }
-          }
-        };
-
-        xhr.open( 'GET', url, false );
-
-        try {
-          xhr.send();
-        }
-        catch ( e ) {
-          alert( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
-        }
-
-      }
-      else if ( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) {
-
-        section.outerHTML = slidify( getMarkdownFromSlide( section ), {
-          separator: section.getAttribute( 'data-separator' ),
-          verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-          notesSeparator: section.getAttribute( 'data-separator-notes' ),
-          attributes: getForwardedAttributes( section )
-        });
-
-      }
-      else {
-        section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) );
-      }
-    }
-
-  }
-
-  /**
-   * Check if a node value has the attributes pattern.
-   * If yes, extract it and add that value as one or several attributes
-   * the the terget element.
-   *
-   * You need Cache Killer on Chrome to see the effect on any FOM transformation
-   * directly on refresh (F5)
-   * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
-   */
-  function addAttributeInElement( node, elementTarget, separator ) {
-
-    let mardownClassesInElementsRegex = new RegExp( separator, 'mg' );
-    let mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"=]+?)\"", 'mg' );
-    let nodeValue = node.nodeValue;
-    let matches = mardownClassesInElementsRegex.exec( nodeValue );
-    if ( matches != null ) {
-
-      let classes = matches[1];
-      nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex );
-      node.nodeValue = nodeValue;
-      let matchesClass;
-      while ( (matchesClass = mardownClassRegex.exec( classes )) != null ) {
-        elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
-      }
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Add attributes to the parent element of a text node,
-   * or the element of an attribute node.
-   */
-  function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
-
-    if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) {
-      let previousParentElement = element;
-      for ( let i = 0; i < element.childNodes.length; i++ ) {
-        let childElement = element.childNodes[i];
-        if ( i > 0 ) {
-          let j = i - 1;
-          while ( j >= 0 ) {
-            let aPreviousChildElement = element.childNodes[j];
-            if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != 'BR' ) {
-              previousParentElement = aPreviousChildElement;
-              break;
-            }
-            j = j - 1;
-          }
-        }
-        let parentSection = section;
-        if ( childElement.nodeName ==  'section' ) {
-          parentSection = childElement ;
-          previousParentElement = childElement ;
-        }
-        if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) {
-          addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
-        }
-      }
-    }
-
-    if ( element.nodeType == Node.COMMENT_NODE ) {
-      if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) {
-        addAttributeInElement( element, section, separatorSectionAttributes );
-      }
-    }
-  }
-
-  /**
-   * Converts any current data-markdown slides in the
-   * DOM to HTML.
-   */
-  function convertSlides() {
-
-    let sections = document.querySelectorAll( '[data-markdown]');
-
-    for ( let i = 0, len = sections.length; i < len; i++ ) {
-
-      let section = sections[i];
-
-      // Only parse the same slide once
-      if ( !section.getAttribute( 'data-markdown-parsed' ) ) {
-
-        section.setAttribute( 'data-markdown-parsed', true );
-
-        let notes = section.querySelector( 'aside.notes' );
-        let markdown = getMarkdownFromSlide( section );
-
-        section.innerHTML = marked( markdown );
-        addAttributes(   section, section, null, section.getAttribute( 'data-element-attributes' ) ||
-                section.parentNode.getAttribute( 'data-element-attributes' ) ||
-                DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-        section.getAttribute( 'data-attributes' ) ||
-                section.parentNode.getAttribute( 'data-attributes' ) ||
-                DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
-
-        // If there were notes, we need to re-add them after
-        // having overwritten the section's HTML
-        if ( notes ) {
-          section.appendChild( notes );
-        }
-
-      }
-
-    }
-
-  }
-
-  // API
-  return {
-    getMarkdownFromSlide: getMarkdownFromSlide,
-    createMarkdownSlide: createMarkdownSlide,
-    processSlides: processSlides,
-    addAttributes: addAttributes,
-    convertSlides: convertSlides
-  };
-}

+ 0 - 5
packages/app/src/client/util/smooth-scroll.ts

@@ -1,5 +0,0 @@
-// option object for react-scroll
-export const DEFAULT_AUTO_SCROLL_OPTS = {
-  smooth: 'easeOutQuint',
-  duration: 1200,
-};

+ 1 - 1
packages/app/src/client/util/toastr.ts

@@ -17,7 +17,7 @@ export const toastError = (err: string | Error | Error[], option: ToastOptions =
 
   for (const err of errs) {
     const message = (typeof err === 'string') ? err : err.message;
-    toast.error(message || err, option);
+    toast.error(message, option);
   }
 };
 

+ 1 - 1
packages/app/src/components/Admin/App/AwsSetting.tsx

@@ -140,11 +140,11 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
           <input
             className="form-control"
             type="text"
-            defaultValue={props.s3SecretAccessKey || ''}
             onChange={(e) => {
               props?.onChangeS3SecretAccessKey(e.target.value);
             }}
           />
+          <p className="form-text text-muted">{t('admin:app_setting.s3_secret_access_key_input_description')}</p>
         </div>
       </div>
 

+ 9 - 8
packages/app/src/components/Admin/App/ConfirmModal.tsx

@@ -1,15 +1,15 @@
 import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-import { TFunctionResult } from 'i18next';
 
 type ConfirmModalProps = {
   isModalOpen: boolean
-  warningMessage: TFunctionResult
-  supplymentaryMessage: TFunctionResult | null
-  confirmButtonTitle: TFunctionResult
+  warningMessage: string
+  supplymentaryMessage: string | null
+  confirmButtonTitle: string
   onConfirm?: () => Promise<void>
   onCancel?: () => void
 };
@@ -43,13 +43,14 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
               <br />
               <br />
               <span className="text-warning">
-                <i className="icon-exclamation icon-fw"></i>
-                {props.supplymentaryMessage}
+                <>
+                  <i className="icon-exclamation icon-fw"></i>
+                  {props.supplymentaryMessage}
+                </>
               </span>
             </>
           )
         }
-
       </ModalBody>
       <ModalFooter>
         <button

+ 3 - 3
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -29,15 +29,15 @@ const AdminNavigation = (props) => {
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
       case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
       case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
+      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
       case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
       case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
       case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
-      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{     t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
+      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */
@@ -87,6 +87,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="customize"    isListGroupItems isActive={isActiveMenu('/customize')} />
         <MenuLink menu="importer"     isListGroupItems isActive={isActiveMenu('/importer')} />
         <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
+        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
         <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
         <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
@@ -94,7 +95,6 @@ const AdminNavigation = (props) => {
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="plugins"      isListGroupItems isActive={isActiveMenu('/plugins')} />
-        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (

+ 0 - 8
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -12,7 +12,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
-import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
 const logger = loggerFactory('growi:MarkDown');
@@ -56,13 +55,6 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
       </Card>
       <IndentForm />
 
-      {/* Presentation Setting */}
-      <h2 className="admin-setting-header">{ t('markdown_settings.presentation_header') }</h2>
-      <Card className="card well my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.presentation_desc') }</CardBody>
-      </Card>
-      <PresentationForm />
-
       {/* XSS Setting */}
       <h2 className="admin-setting-header">{ t('markdown_settings.xss_header') }</h2>
       <Card className="card well my-3">

+ 0 - 143
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,143 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-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('toaster.update_successed', { target: t('markdown_settings.presentation_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
-
-    return (
-      <fieldset className="form-group col-12 my-2">
-
-        <label className="col-8 offset-4 col-form-label font-weight-bold text-left mt-3">
-          {t('markdown_settings.presentation_options.page_break_setting')}
-        </label>
-
-        <div className="form-group col-12 my-3">
-          <div className="row">
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="pageBreakOption1"
-                  checked={pageBreakSeparator === 1}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption1">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.preset_one_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.preset_one_separator_desc') }
-                    <input
-                      className="form-control"
-                      type="text"
-                      value={t('markdown_settings.presentation_options.preset_one_separator_value')}
-                      readOnly
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="pageBreakOption2"
-                  checked={pageBreakSeparator === 2}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption2">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.preset_two_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.preset_two_separator_desc') }
-                    <input
-                      className="form-control"
-                      type="text"
-                      value={t('markdown_settings.presentation_options.preset_two_separator_value')}
-                      readOnly
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  id="pageBreakOption3"
-                  className="custom-control-input"
-                  checked={pageBreakSeparator === 3}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption3">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.custom_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.custom_separator_desc') }
-                    <input
-                      className="form-control"
-                      defaultValue={pageBreakCustomSeparator}
-                      onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
-      </fieldset>
-    );
-  }
-
-}
-
-PresentationForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-const PresentationFormWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <PresentationForm t={t} {...props} />;
-};
-
-const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
-
-export default PresentationFormWrapper;

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -2,7 +2,6 @@ import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
 
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -30,7 +29,7 @@ type AvailableOption = {
   actionForPages: string,
   iconClass: string,
   styleClass: string,
-  label: TFunctionResult,
+  label: string,
 };
 
 // actionName master constants

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,7 +1,6 @@
 import React, { FC, useCallback, useState } from 'react';
 
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 import { IUserGroupHasId } from '~/interfaces/user';
@@ -9,7 +8,7 @@ import { IUserGroupHasId } from '~/interfaces/user';
 type Props = {
   userGroup: IUserGroupHasId,
   selectableParentUserGroups?: IUserGroupHasId[],
-  submitButtonLabel: TFunctionResult;
+  submitButtonLabel: string;
   onSubmit?: (targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>) => Promise<void> | void
 };
 

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -3,7 +3,6 @@ import React, {
 } from 'react';
 
 import { Ref } from '@growi/core';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -13,7 +12,7 @@ import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 
 type Props = {
   userGroup?: IUserGroupHasId,
-  buttonLabel?: TFunctionResult,
+  buttonLabel?: string,
   onClickSubmit?: (userGroupData: Partial<IUserGroupHasId>) => Promise<IUserGroupHasId | void>
   isShow?: boolean
   onHide?: () => Promise<void> | void

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -4,12 +4,11 @@ import React, {
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
-  headerLabel?: TFunctionResult,
+  headerLabel?: string,
   userGroups: IUserGroupHasId[],
   userGroupRelations: IUserGroupRelation[],
   childUserGroups: IUserGroupHasId[],

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -13,7 +13,7 @@ type Props = {
 }
 
 export const UserGroupUserTable = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const {
     userGroupRelations, onClickRemoveUserBtn, onClickPlusBtn,

+ 5 - 11
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -9,7 +9,6 @@ import {
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class PasswordResetModal extends React.Component {
 
@@ -17,7 +16,6 @@ class PasswordResetModal extends React.Component {
     super(props);
 
     this.state = {
-      temporaryPassword: [],
       isPasswordResetDone: false,
     };
 
@@ -27,9 +25,8 @@ class PasswordResetModal extends React.Component {
   async resetPassword() {
     const { t, userForPasswordResetModal } = this.props;
     try {
-      const res = await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
-      const { newPassword } = res.data;
-      this.setState({ temporaryPassword: newPassword, isPasswordResetDone: true });
+      await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
+      this.setState({ isPasswordResetDone: true });
     }
     catch (err) {
       toastError(err, t('toaster.failed_to_reset_password'));
@@ -42,8 +39,8 @@ class PasswordResetModal extends React.Component {
     return (
       <>
         <p>
-          {t('user_management.reset_password_modal.password_never_seen')}<br />
-          <span className="text-danger">{t('user_management.reset_password_modal.send_new_password')}</span>
+          {t('user_management.reset_password_modal.reset_password_info')}<br />
+          <span className="text-danger">{t('user_management.reset_password_modal.reset_password_alert')}</span>
         </p>
         <p>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
@@ -57,13 +54,10 @@ class PasswordResetModal extends React.Component {
 
     return (
       <>
-        <p className="alert alert-danger">{t('user_management.reset_password_modal.password_reset_message')}</p>
+        <p className="text-danger">{t('user_management.reset_password_modal.password_reset_message')}</p>
         <p>
           {t('user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
-        <p>
-          {t('user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
-        </p>
       </>
     );
   }

+ 62 - 27
packages/app/src/components/Comments.tsx

@@ -1,63 +1,98 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 
-import { IRevisionHasId } from '@growi/core';
+import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import { PageCommentProps } from '~/components/PageComment';
+import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage } from '~/stores/page';
 
 import { useCurrentUser } from '../stores/context';
 
-import { CommentEditorProps } from './PageComment/CommentEditor';
+import type { CommentEditorProps } from './PageComment/CommentEditor';
+
+
+const { isTopPage } = pagePathUtils;
+
 
 const PageComment = dynamic<PageCommentProps>(() => import('~/components/PageComment').then(mod => mod.PageComment), { ssr: false });
 const CommentEditor = dynamic<CommentEditorProps>(() => import('./PageComment/CommentEditor').then(mod => mod.CommentEditor), { ssr: false });
 
-type CommentsProps = {
+export type CommentsProps = {
   pageId: string,
   pagePath: string,
   revision: IRevisionHasId,
+  onLoaded?: () => void,
 }
 
 export const Comments = (props: CommentsProps): JSX.Element => {
 
-  const { pageId, pagePath, revision } = props;
+  const {
+    pageId, pagePath, revision, onLoaded,
+  } = props;
 
   const { mutate } = useSWRxPageComment(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
 
-  if (pageId == null) {
+  const pageCommentParentRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const parent = pageCommentParentRef.current;
+    if (parent == null) return;
+
+    const observerCallback = (mutationRecords:MutationRecord[]) => {
+      mutationRecords.forEach((record:MutationRecord) => {
+        const target = record.target as HTMLElement;
+
+        for (const child of Array.from(target.children)) {
+          const childId = (child as HTMLElement).id;
+          if (childId === PageCommentRootElemId) {
+            onLoaded?.();
+            break;
+          }
+        }
+
+      });
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(parent, { childList: true });
+    return () => {
+      observer.disconnect();
+    };
+  }, [onLoaded]);
+
+  const isTopPagePath = isTopPage(pagePath);
+
+  if (pageId == null || isTopPagePath) {
     return <></>;
   }
 
   return (
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
-        <div className="page-comments">
-          <div id="page-comments-list" className="page-comments-list">
-            <PageComment
+        <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
+          <PageComment
+            pageId={pageId}
+            pagePath={pagePath}
+            revision={revision}
+            currentUser={currentUser}
+            isReadOnly={false}
+            titleAlign="left"
+            hideIfEmpty={false}
+          />
+        </div>
+        { !isDeleted && (
+          <div id="page-comment-write">
+            <CommentEditor
               pageId={pageId}
-              pagePath={pagePath}
-              revision={revision}
-              currentUser={currentUser}
-              isReadOnly={false}
-              titleAlign="left"
-              hideIfEmpty={false}
+              isForNewComment
+              onCommentButtonClicked={mutate}
+              revisionId={revision._id}
             />
           </div>
-          { !isDeleted && (
-            <div id="page-comment-write">
-              <CommentEditor
-                pageId={pageId}
-                isForNewComment
-                onCommentButtonClicked={mutate}
-                revisionId={revision._id}
-              />
-            </div>
-          )}
-        </div>
+        )}
       </div>
     </div>
   );

+ 37 - 0
packages/app/src/components/Common/LazyRenderer.tsx

@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react';
+
+type Props = {
+  shouldRender: boolean | (() => boolean),
+  children: JSX.Element,
+}
+
+export const LazyRenderer = (props: Props): JSX.Element => {
+  const { shouldRender: _shouldRender, children } = props;
+
+  const [isActivated, setActivated] = useState(false);
+
+  const shouldRender = typeof _shouldRender === 'function'
+    ? _shouldRender()
+    : _shouldRender;
+
+  useEffect(() => {
+    if (isActivated) {
+      return;
+    }
+    setActivated(shouldRender);
+  }, [isActivated, shouldRender]);
+
+  const additionalClassName = shouldRender ? '' : 'd-none';
+
+  if (!isActivated) {
+    return <></>;
+  }
+
+  return (
+    <>
+      { React.cloneElement(children, {
+        className: `${children.props.className ?? ''} ${additionalClassName}`,
+      }) }
+    </>
+  );
+};

+ 2 - 5
packages/app/src/components/ContentLinkButtons.tsx

@@ -3,17 +3,14 @@ import React from 'react';
 import { IUserHasId } from '@growi/core';
 import { Link as ScrollLink } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 
 import styles from './ContentLinkButtons.module.scss';
 
-const OFFSET = -120;
-
 const BookMarkLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="bookmarks-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="bookmarks-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-2"
@@ -30,7 +27,7 @@ BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 const RecentlyCreatedLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="recently-created-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="recently-created-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-3"

+ 7 - 22
packages/app/src/components/CustomNavigation/CustomTabContent.tsx

@@ -1,41 +1,35 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 
-import PropTypes from 'prop-types';
 import {
   TabContent, TabPane,
 } from 'reactstrap';
 
-import { ICustomNavTabMappings } from '~/interfaces/ui';
+import type { ICustomNavTabMappings } from '~/interfaces/ui';
+
+import { LazyRenderer } from '../Common/LazyRenderer';
 
 
 type Props = {
   activeTab: string,
   navTabMapping: ICustomNavTabMappings,
   additionalClassNames?: string[],
-
 }
 
 const CustomTabContent = (props: Props): JSX.Element => {
 
   const { activeTab, navTabMapping, additionalClassNames } = props;
 
-  const [activatedContent, setActivatedContent] = useState(new Set([activeTab]));
-
-  // add activated content to Set
-  useEffect(() => {
-    setActivatedContent(activatedContent.add(activeTab));
-  }, [activatedContent, activeTab]);
-
   return (
     <TabContent activeTab={activeTab} className={additionalClassNames != null ? additionalClassNames.join(' ') : ''}>
       {Object.entries(navTabMapping).map(([key, value]) => {
 
-        const shouldRender = key === activeTab || activatedContent.has(key);
         const { Content } = value;
 
         return (
           <TabPane key={key} tabId={key}>
-            { shouldRender && <Content /> }
+            <LazyRenderer shouldRender={key === activeTab}>
+              <Content />
+            </LazyRenderer>
           </TabPane>
         );
       })}
@@ -44,13 +38,4 @@ const CustomTabContent = (props: Props): JSX.Element => {
 
 };
 
-CustomTabContent.propTypes = {
-  activeTab: PropTypes.string.isRequired,
-  navTabMapping: PropTypes.object.isRequired,
-  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
-};
-CustomTabContent.defaultProps = {
-  additionalClassNames: [],
-};
-
 export default CustomTabContent;

+ 13 - 46
packages/app/src/components/DescendantsPageList.tsx

@@ -11,15 +11,14 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
-  useIsGuestUser, useIsSharedUser, useShowPageLimitationXL,
+  useIsGuestUser, useIsSharedUser,
 } from '~/stores/context';
-import { useIsTrashPage } from '~/stores/page';
 import {
-  usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
+  mutatePageTree,
   useSWRxPageInfoForList, useSWRxPageList,
 } from '~/stores/page-listing';
 
-import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
+import { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 
@@ -37,7 +36,7 @@ const convertToIDataWithMeta = (page: IPageHasId): IDataWithMeta<IPageHasId> =>
   return { data: page };
 };
 
-export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
+const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
 
   const { t } = useTranslation();
 
@@ -52,10 +51,6 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfoForOperation>[] = [];
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-
   // initial data
   if (pagingResult != null) {
     // convert without meta at first
@@ -74,23 +69,22 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
       toastSuccess(t('deleted_pages_completely', { path }));
     }
 
-    advancePt();
+    mutatePageTree();
 
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
     }
-  }, [advancePt, onPagesDeleted, t]);
+  }, [onPagesDeleted, t]);
 
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
 
-    advancePt();
-    advanceDpl();
+    mutatePageTree();
 
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
     }
-  }, [advanceDpl, advancePt, onPagePutBacked, t]);
+  }, [onPagePutBacked, t]);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
@@ -135,43 +129,18 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
 export type DescendantsPageListProps = {
   path: string,
+  limit?: number,
+  forceHideMenuItems?: ForceHideMenuItems,
 }
 
 export const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const { path } = props;
+  const { path, limit, forceHideMenuItems } = props;
 
   const [activePage, setActivePage] = useState(1);
 
   const { data: isSharedUser } = useIsSharedUser();
 
-  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage);
-
-  if (error != null) {
-    return (
-      <div className="my-5">
-        <div className="text-danger">{error.message}</div>
-      </div>
-    );
-  }
-
-  return (
-    <DescendantsPageListSubstance
-      pagingResult={pagingResult}
-      activePage={activePage}
-      setActivePage={setActivePage}
-      onPagesDeleted={() => mutate()}
-      onPagePutBacked={() => mutate()}
-    />
-  );
-};
-
-export const DescendantsPageListForCurrentPath = (): JSX.Element => {
-
-  const [activePage, setActivePage] = useState(1);
-
-  const { data: isTrashPage } = useIsTrashPage();
-  const { data: limit } = useShowPageLimitationXL();
-  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage, limit);
+  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage, limit);
 
   if (error != null) {
     return (
@@ -181,8 +150,6 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
     );
   }
 
-  const forceHideMenuItems = isTrashPage ? [MenuItemType.RENAME] : undefined;
-
   return (
     <DescendantsPageListSubstance
       pagingResult={pagingResult}
@@ -190,7 +157,7 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
       setActivePage={setActivePage}
       forceHideMenuItems={forceHideMenuItems}
       onPagesDeleted={() => mutate()}
+      onPagePutBacked={() => mutate()}
     />
   );
-
 };

+ 13 - 9
packages/app/src/components/DescendantsPageListModal.tsx

@@ -1,8 +1,9 @@
 
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
 import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
@@ -19,15 +20,9 @@ import TimeLineIcon from './Icons/TimeLineIcon';
 
 import styles from './DescendantsPageListModal.module.scss';
 
-const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
-  return <DescendantsPageList {...props}/>;
-};
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
 
-const PageTimeline = (): JSX.Element => {
-  const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
-  return <PageTimeline />;
-};
+const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
 export const DescendantsPageListModal = (): JSX.Element => {
   const { t } = useTranslation();
@@ -39,6 +34,15 @@ export const DescendantsPageListModal = (): JSX.Element => {
 
   const { data: status, close } = useDescendantsPageListModal();
 
+  const { events } = useRouter();
+
+  useEffect(() => {
+    events.on('routeChangeStart', close);
+    return () => {
+      events.off('routeChangeStart', close);
+    };
+  }, [close, events]);
+
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {

+ 1 - 2
packages/app/src/components/Fab.tsx

@@ -6,7 +6,6 @@ import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
@@ -96,7 +95,7 @@ export const Fab = (): JSX.Element => {
 
   const ScrollToTopButton = useCallback(() => {
     const clickHandler = () => {
-      animateScroll.scrollToTop(DEFAULT_AUTO_SCROLL_OPTS);
+      animateScroll.scrollToTop({ duration: 200 });
     };
 
     return (

+ 0 - 14
packages/app/src/components/Invited.module.scss

@@ -1,14 +0,0 @@
-.invited,
-.nologin.error {
-  .main .row {
-    @media (min-width: 510px) {
-      .offset-sm-4 {
-        margin-left: calc(50% - 240px);
-      }
-
-      .col-sm-4 {
-        width: 480px;
-      }
-    }
-  }
-}

+ 0 - 11
packages/app/src/components/Layout/BasicLayout.tsx

@@ -4,7 +4,6 @@ import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 
-import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 
@@ -68,13 +67,3 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
     </RawLayout>
   );
 };
-
-export const BasicLayoutWithEditorMode = ({ children }: Props): JSX.Element => {
-  const className = useEditorModeClassName();
-
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
-};

+ 1 - 1
packages/app/src/components/Layout/MainPane.tsx

@@ -20,7 +20,7 @@ export const MainPane = (props: Props): JSX.Element => {
           <div id="content-main" className="content-main grw-container-convertible">
             { sideContents != null
               ? (
-                <div className="d-flex flex-column flex-lg-row">
+                <div className="d-flex flex-column flex-column-reverse flex-lg-row">
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                     {children}
                   </div>

+ 0 - 5
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -14,11 +14,6 @@
     .main {
       width: 100vw;
 
-      > .row {
-        margin-right: 20px;
-        margin-left: 20px;
-      }
-
       .nologin-header {
         display: flex;
         flex-direction: column;

+ 1 - 6
packages/app/src/components/LoginForm.tsx

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useCallback,
 } from 'react';
 
-import { USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
@@ -95,16 +94,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
     try {
       const res = await apiv3Post('/login', { loginForm });
-      const { redirectTo, userStatus } = res.data;
+      const { redirectTo } = res.data;
 
       if (redirectTo != null) {
         return router.push(redirectTo);
       }
 
-      if (userStatus !== USER_STATUS.ACTIVE) {
-        window.location.href = '/';
-      }
-
       return router.push('/');
     }
     catch (err) {

+ 1 - 1
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -120,7 +120,7 @@ export const BasicInfoSettings = (): JSX.Element => {
                     checked={personalSettingsInfo?.lang === locale}
                     onChange={() => changePersonalSettingsHandler({ lang: locale })}
                   />
-                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
                 </div>
               );
             })

+ 44 - 39
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -25,7 +25,7 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
   const { isAuthenticated } = props;
 
   const {
-    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+    setTheme, resolvedTheme, useOsSettings, isDarkMode, isForcedByGrowiTheme,
   } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
@@ -114,55 +114,60 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
         {/* sidebar mode */}
         {renderSidebarModeSwitch(false)}
-        {dropdownDivider}
 
         {/* side bar mode on editor */}
         {isAuthenticated && (
           <>
-            {renderSidebarModeSwitch(true)}
             {dropdownDivider}
+            {renderSidebarModeSwitch(true)}
           </>
         )}
 
         {/* color mode */}
-        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-        <form className="px-4">
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto d-flex align-items-center">
-              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <SunIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swUserPreference"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={isDarkMode}
-                  disabled={useOsSettings}
-                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+        { !isForcedByGrowiTheme && (
+          <>
+            {dropdownDivider}
+            <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
+            <form className="px-4">
+              <div className="form-row justify-content-center">
+                <div className="form-group col-auto d-flex align-items-center">
+                  <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <SunIcon />
+                  </IconWithTooltip>
+                  <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                    <input
+                      id="swUserPreference"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={isDarkMode}
+                      disabled={useOsSettings}
+                      onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label" htmlFor="swUserPreference"></label>
+                  </div>
+                  <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                    <MoonIcon />
+                  </IconWithTooltip>
+                </div>
               </div>
-              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                <MoonIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-          <div className="form-row">
-            <div className="form-group col-auto">
-              <div className="custom-control custom-checkbox">
-                <input
-                  id="cbFollowOs"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={useOsSettings}
-                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+              <div className="form-row">
+                <div className="form-group col-auto">
+                  <div className="custom-control custom-checkbox">
+                    <input
+                      id="cbFollowOs"
+                      className="custom-control-input"
+                      type="checkbox"
+                      checked={useOsSettings}
+                      onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                    />
+                    <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+                  </div>
+                </div>
               </div>
-            </div>
-          </div>
-        </form>
+            </form>
+          </>
+        ) }
+
       </div>
 
     </>

Некоторые файлы не были показаны из-за большого количества измененных файлов