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

Merge branch 'master' into imprv/integrate-customize-user-page-delete

ryoji-s 3 лет назад
Родитель
Сommit
e99582bbee
100 измененных файлов с 978 добавлено и 1391 удалено
  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. 78 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. 14 15
      packages/app/package.json
  18. 4 16
      packages/app/public/static/locales/en_US/admin.json
  19. 1 2
      packages/app/public/static/locales/en_US/translation.json
  20. 4 16
      packages/app/public/static/locales/ja_JP/admin.json
  21. 1 1
      packages/app/public/static/locales/ja_JP/commons.json
  22. 1 2
      packages/app/public/static/locales/ja_JP/translation.json
  23. 4 16
      packages/app/public/static/locales/zh_CN/admin.json
  24. 1 2
      packages/app/public/static/locales/zh_CN/translation.json
  25. 6 6
      packages/app/resource/locales/en_US/admin/userInvitation.txt
  26. 11 0
      packages/app/resource/locales/en_US/admin/userResetPassword.txt
  27. 8 8
      packages/app/resource/locales/en_US/admin/userWaitingActivation.txt
  28. 3 3
      packages/app/resource/locales/en_US/notifications/comment.txt
  29. 4 4
      packages/app/resource/locales/en_US/notifications/notActiveUser.txt
  30. 2 2
      packages/app/resource/locales/en_US/notifications/pageCreate.txt
  31. 2 2
      packages/app/resource/locales/en_US/notifications/pageDelete.txt
  32. 2 2
      packages/app/resource/locales/en_US/notifications/pageEdit.txt
  33. 2 2
      packages/app/resource/locales/en_US/notifications/pageLike.txt
  34. 2 2
      packages/app/resource/locales/en_US/notifications/pageMove.txt
  35. 4 4
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  36. 1 1
      packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt
  37. 4 4
      packages/app/resource/locales/en_US/notifications/userActivation.txt
  38. 6 6
      packages/app/resource/locales/ja_JP/admin/userInvitation.txt
  39. 12 0
      packages/app/resource/locales/ja_JP/admin/userResetPassword.txt
  40. 8 8
      packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt
  41. 4 4
      packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt
  42. 4 4
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  43. 1 1
      packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt
  44. 4 4
      packages/app/resource/locales/ja_JP/notifications/userActivation.txt
  45. 6 6
      packages/app/resource/locales/zh_CN/admin/userInvitation.txt
  46. 11 0
      packages/app/resource/locales/zh_CN/admin/userResetPassword.txt
  47. 8 8
      packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt
  48. 3 3
      packages/app/resource/locales/zh_CN/notifications/comment.txt
  49. 4 4
      packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt
  50. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageCreate.txt
  51. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageDelete.txt
  52. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageEdit.txt
  53. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageLike.txt
  54. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageMove.txt
  55. 3 3
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  56. 1 1
      packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt
  57. 4 4
      packages/app/resource/locales/zh_CN/notifications/userActivation.txt
  58. 0 42
      packages/app/src/client/legacy/crowi-presentation.js
  59. 0 1
      packages/app/src/client/services/AdminAppContainer.js
  60. 0 35
      packages/app/src/client/services/AdminMarkDownContainer.js
  61. 5 11
      packages/app/src/client/services/layout.ts
  62. 2 2
      packages/app/src/client/services/page-operation.ts
  63. 8 2
      packages/app/src/client/services/side-effects/page-updated.ts
  64. 1 6
      packages/app/src/client/services/user-ui-settings.ts
  65. 60 0
      packages/app/src/client/util/locale-utils.ts
  66. 0 107
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  67. 0 379
      packages/app/src/client/util/reveal/plugins/markdown.js
  68. 0 5
      packages/app/src/client/util/smooth-scroll.ts
  69. 1 1
      packages/app/src/components/Admin/App/AwsSetting.tsx
  70. 0 8
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  71. 0 143
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  72. 5 11
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  73. 52 23
      packages/app/src/components/Comments.tsx
  74. 2 5
      packages/app/src/components/ContentLinkButtons.tsx
  75. 13 46
      packages/app/src/components/DescendantsPageList.tsx
  76. 2 8
      packages/app/src/components/DescendantsPageListModal.tsx
  77. 1 2
      packages/app/src/components/Fab.tsx
  78. 7 6
      packages/app/src/components/InfiniteScroll.tsx
  79. 0 14
      packages/app/src/components/Invited.module.scss
  80. 0 5
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  81. 1 6
      packages/app/src/components/LoginForm.tsx
  82. 44 39
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  83. 12 17
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  84. 11 4
      packages/app/src/components/NotFoundPage.tsx
  85. 0 91
      packages/app/src/components/Page/PageContents.tsx
  86. 39 0
      packages/app/src/components/Page/PageContentsUtilities.tsx
  87. 79 38
      packages/app/src/components/Page/PageView.tsx
  88. 22 58
      packages/app/src/components/Page/RevisionLoader.tsx
  89. 28 3
      packages/app/src/components/PageAccessoriesModal.tsx
  90. 1 1
      packages/app/src/components/PageAlert/PageAlerts.tsx
  91. 3 3
      packages/app/src/components/PageAlert/PageGrantAlert.tsx
  92. 3 2
      packages/app/src/components/PageComment/Comment.module.scss
  93. 7 5
      packages/app/src/components/PageComment/Comment.tsx
  94. 11 6
      packages/app/src/components/PageComment/CommentEditor.tsx
  95. 8 0
      packages/app/src/components/PageDeleteModal.tsx
  96. 39 19
      packages/app/src/components/PageEditor.tsx
  97. 9 8
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  98. 6 2
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  99. 1 5
      packages/app/src/components/PageEditor/Preview.tsx
  100. 11 10
      packages/app/src/components/PageEditorByHackmd.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

+ 78 - 1
CHANGELOG.md

@@ -1,9 +1,86 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.7...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.0.7](https://github.com/weseek/growi/compare/v6.0.6...v6.0.7) - 2023-02-21
+
+### 💎 Features
+
+- feat: Manage guest ui setting with session (#7401) @yukendev
+- feat: Manage guest sidebar mode with session (#7393) @yukendev
+
+### 🚀 Improvement
+
+- imprv: UnsavedAlertDialog and page transition when next routing (#7400) @miya
+- imprv: PasswordResetModal styles and ejs format (#7404) @jam411
+- imprv: Initialize UserUISettings (#7397) @yuki-takei
+- imprv: Presentation behavior (#7399) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: PageStatusAlert is displayed on unnecessary pages (#7413) @miya
+- fix: PageStatusAlert does not disappear after loading latest revision (#7412) @miya
+- fix: Unable to transition requested page after login (#7402) @miya
+- fix: Page body is blank when opening editor after duplicating page (#7394) @miya
+- fix: Error when pressing the conflict resolution button on PageStatusAlert (#7395) @miya
+- fix: mono-blue subnavigation color (#7398) @ayaka0417
+- imprv: Add send email to user feat to `/reset-password` endpoint v6 (#7356) @jam411
+- fix: Behavior when color schema is forced by GROWI themes (#7391) @yuki-takei
+- fix: Sidebar mode on editor doesn't work in HackMD tab (#7396) @yuki-takei
+- fix: Can't controll slack notification button in comment editor (#7389) @yukendev
+
+## [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

+ 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.6-RC.0",
+  "version": "6.0.8-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-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.5`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.5/packages/app/docker/Dockerfile)
+* [`6.0.7`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.7/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)

+ 14 - 15
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.6-RC.0",
+  "version": "6.0.8-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -67,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.6-RC.0",
-    "@growi/core": "^6.0.6-RC.0",
-    "@growi/hackmd": "^6.0.6-RC.0",
-    "@growi/preset-themes": "^6.0.6-RC.0",
-    "@growi/remark-drawio": "^6.0.6-RC.0",
-    "@growi/remark-growi-directive": "^6.0.6-RC.0",
-    "@growi/remark-lsx": "^6.0.6-RC.0",
-    "@growi/slack": "^6.0.6-RC.0",
+    "@growi/codemirror-textlint": "^6.0.8-RC.0",
+    "@growi/core": "^6.0.8-RC.0",
+    "@growi/hackmd": "^6.0.8-RC.0",
+    "@growi/preset-themes": "^6.0.8-RC.0",
+    "@growi/remark-drawio": "^6.0.8-RC.0",
+    "@growi/remark-growi-directive": "^6.0.8-RC.0",
+    "@growi/remark-lsx": "^6.0.8-RC.0",
+    "@growi/slack": "^6.0.8-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -97,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 +185,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^2.0.2",
+    "swr": "^2.0.3",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
@@ -203,7 +203,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.6-RC.0",
+    "@growi/presentation": "^6.0.8-RC.0",
+    "@growi/ui": "^6.0.8-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
@@ -237,14 +238,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",
@@ -407,19 +408,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": {
@@ -753,9 +741,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 - 2
packages/app/public/static/locales/en_US/translation.json

@@ -58,7 +58,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",
@@ -329,7 +328,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": "無効",
@@ -415,19 +416,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": {
@@ -761,9 +749,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": "新しいパスワード"
     },

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

@@ -104,7 +104,7 @@
     "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

@@ -55,7 +55,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": "いいねをしているユーザーはいません",
@@ -329,7 +328,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": "停用",
@@ -415,19 +416,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": {
@@ -761,9 +749,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 - 2
packages/app/public/static/locales/zh_CN/translation.json

@@ -55,7 +55,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": "还没有用户喜欢这个",
@@ -319,7 +318,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);

+ 8 - 2
packages/app/src/client/services/side-effects/page-updated.ts

@@ -1,6 +1,7 @@
 import { useCallback, useEffect } from 'react';
 
 import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageId } from '~/stores/context';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useGlobalSocket } from '~/stores/websocket';
 
@@ -9,6 +10,7 @@ export const usePageUpdatedEffect = (): void => {
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
   const { data: socket } = useGlobalSocket();
+  const { data: currentPageId } = useCurrentPageId();
 
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
@@ -21,8 +23,12 @@ export const usePageUpdatedEffect = (): void => {
       revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
       hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
-    setRemoteLatestPageData(remoteData);
-  }, [setRemoteLatestPageData]);
+
+    if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {
+      setRemoteLatestPageData(remoteData);
+    }
+
+  }, [currentPageId, setRemoteLatestPageData]);
 
   // listen socket for someone updating this page
   useEffect(() => {

+ 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/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>
 

+ 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;

+ 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>
       </>
     );
   }

+ 52 - 23
packages/app/src/components/Comments.tsx

@@ -1,9 +1,9 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import type { 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';
 
@@ -22,16 +22,47 @@ 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();
 
+  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) {
@@ -41,29 +72,27 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   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>
   );

+ 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"

+ 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()}
     />
   );
-
 };

+ 2 - 8
packages/app/src/components/DescendantsPageListModal.tsx

@@ -20,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();

+ 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 (

+ 7 - 6
packages/app/src/components/Sidebar/InfiniteScroll.tsx → packages/app/src/components/InfiniteScroll.tsx

@@ -1,14 +1,15 @@
 import React, {
   Ref, useEffect, useState,
 } from 'react';
+
 import type { SWRInfiniteResponse } from 'swr/infinite';
 
 type Props<T> = {
-  swrInifiniteResponse : SWRInfiniteResponse<T>
-  children: React.ReactChild | ((item: T) => React.ReactNode),
+  swrInifiniteResponse: SWRInfiniteResponse<T>
+  children: React.ReactNode,
   loadingIndicator?: React.ReactNode
   endingIndicator?: React.ReactNode
-  isReachingEnd?: boolean,
+  isReachingEnd?: boolean
   offset?: number
 }
 
@@ -39,7 +40,7 @@ const LoadingIndicator = (): React.ReactElement => {
 const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
   const {
     swrInifiniteResponse: {
-      setSize, data, isValidating,
+      setSize, isValidating,
     },
     children,
     loadingIndicator,
@@ -54,11 +55,11 @@ const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
     if (intersecting && !isValidating && !isReachingEnd) {
       setSize(size => size + 1);
     }
-  }, [setSize, intersecting]);
+  }, [setSize, intersecting, isValidating, isReachingEnd]);
 
   return (
     <>
-      {typeof children === 'function' ? data?.map(item => children(item)) : children}
+      {children}
       <div style={{ position: 'relative' }}>
         <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
         {isReachingEnd

+ 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 - 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) {

+ 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>
 
     </>

+ 12 - 17
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -24,7 +24,7 @@ import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -36,7 +36,6 @@ import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
 import { NotAvailable } from '../NotAvailable';
-import { NotAvailableForNow } from '../NotAvailableForNow';
 import { Skeleton } from '../Skeleton';
 
 import type { AuthorInfoProps } from './AuthorInfo';
@@ -85,23 +84,19 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
 
-  const hrefForPresentationModal = `${pageId}/?presentation=1`;
-
   return (
     <>
       {/* Presentation */}
-      <NotAvailableForNow>
-        <DropdownItem
-          onClick={() => openPresentationModal(hrefForPresentationModal)}
-          data-testid="open-presentation-modal-btn"
-          className="grw-page-control-dropdown-item"
-        >
-          <i className="icon-fw grw-page-control-dropdown-icon">
-            <PresentationIcon />
-          </i>
-          { t('Presentation Mode') }
-        </DropdownItem>
-      </NotAvailableForNow>
+      <DropdownItem
+        onClick={() => openPresentationModal()}
+        data-testid="open-presentation-modal-btn"
+        className="grw-page-control-dropdown-item"
+      >
+        <i className="icon-fw grw-page-control-dropdown-icon">
+          <PresentationIcon />
+        </i>
+        { t('Presentation Mode') }
+      </DropdownItem>
 
       {/* Export markdown */}
       <DropdownItem
@@ -200,7 +195,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const router = useRouter();
 
   const { data: shareLinkId } = useShareLinkId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const { data: currentPathname } = useCurrentPathname();
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');

+ 11 - 4
packages/app/src/components/NotFoundPage.tsx

@@ -3,19 +3,26 @@ import React, { useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import { DescendantsPageList } from './DescendantsPageList';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import { PageTimeline } from './PageTimeline';
 
-const NotFoundPage = (): JSX.Element => {
+
+type NotFoundPageProps = {
+  path: string,
+}
+
+const NotFoundPage = (props: NotFoundPageProps): JSX.Element => {
   const { t } = useTranslation();
 
+  const { path } = props;
+
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
         Icon: PageListIcon,
-        Content: DescendantsPageListForCurrentPath,
+        Content: () => <DescendantsPageList path={path} />,
         i18n: t('page_list'),
         index: 0,
       },
@@ -26,7 +33,7 @@ const NotFoundPage = (): JSX.Element => {
         index: 1,
       },
     };
-  }, [t]);
+  }, [path, t]);
 
   return (
     <div className="d-edit-none">

+ 0 - 91
packages/app/src/components/Page/PageContents.tsx

@@ -1,91 +0,0 @@
-import React, { useEffect } from 'react';
-
-import { pagePathUtils } from '@growi/core';
-import { useTranslation } from 'next-i18next';
-
-import { useUpdateStateAfterSave } from '~/client/services/page-operation';
-import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
-import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useCurrentPathname } from '~/stores/context';
-import { useEditingMarkdown } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
-import { useViewOptions } from '~/stores/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from './RevisionRenderer';
-
-
-const logger = loggerFactory('growi:Page');
-
-
-export const PageContents = (): JSX.Element => {
-  const { t } = useTranslation();
-
-  const { data: currentPathname } = useCurrentPathname();
-  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
-
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-  useHandsontableModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-  useDrawioModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-
-  if (currentPage == null || rendererOptions == null) {
-    const entries = Object.entries({
-      currentPage, rendererOptions,
-    })
-      .map(([key, value]) => [key, value == null ? 'null' : undefined])
-      .filter(([, value]) => value != null);
-
-    logger.warn('Some of materials are missing.', Object.fromEntries(entries));
-
-    return <></>;
-  }
-
-  const { _id: revisionId, body: markdown } = currentPage.revision;
-
-  return (
-    <>
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-    </>
-  );
-
-};

+ 39 - 0
packages/app/src/components/Page/PageContentsUtilities.tsx

@@ -0,0 +1,39 @@
+import { useTranslation } from 'next-i18next';
+
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
+import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useCurrentPageId } from '~/stores/context';
+
+
+export const PageContentsUtilities = (): null => {
+  const { t } = useTranslation();
+
+  const { data: pageId } = useCurrentPageId();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+
+  useHandsontableModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  useDrawioModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  return null;
+};

+ 79 - 38
packages/app/src/components/Page/PageView.tsx

@@ -1,13 +1,21 @@
-import React, { useMemo } from 'react';
+import React, {
+  useEffect, useMemo, useRef, useState,
+} from 'react';
+
 
 import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
   useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
 } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+
 
 import type { CommentsProps } from '../Comments';
 import { MainPane } from '../Layout/MainPane';
@@ -17,7 +25,7 @@ import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
 import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 
-import { PageContents } from './PageContents';
+import RevisionRenderer from './RevisionRenderer';
 
 import styles from './PageView.module.scss';
 
@@ -29,35 +37,69 @@ const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod =>
 const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
 const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
 const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
-
-const IdenticalPathPage = (): JSX.Element => {
-  const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-  return <IdenticalPathPage />;
-};
+const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
 
 
 type Props = {
   pagePath: string,
-  page?: IPagePopulatedToShowRevision,
-  ssrBody?: JSX.Element,
+  rendererConfig: RendererConfig,
+  initialPage?: IPagePopulatedToShowRevision,
 }
 
 export const PageView = (props: Props): JSX.Element => {
+
+  const commentsContainerRef = useRef<HTMLDivElement>(null);
+
+  const [isCommentsLoaded, setCommentsLoaded] = useState(false);
+
   const {
-    pagePath, page, ssrBody,
+    pagePath, initialPage, rendererConfig,
   } = props;
 
-  const pageId = page?._id;
-
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
   const { data: isForbidden } = useIsForbidden();
   const { data: isNotCreatable } = useIsNotCreatable();
-  const { data: isNotFound } = useIsNotFound();
+  const { data: isNotFoundMeta } = useIsNotFound();
   const { data: isMobile } = useIsMobile();
 
+  const { data: pageBySWR } = useSWRxCurrentPage();
+  const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
+
+  const page = pageBySWR ?? initialPage;
+  const isNotFound = isNotFoundMeta || page?.revision == null;
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  // ***************************  Auto Scroll  ***************************
+  useEffect(() => {
+    // do nothing if hash is empty
+    const { hash } = window.location;
+    if (hash.length === 0) {
+      return;
+    }
+
+    const targetId = hash.slice(1);
+
+    const target = document.getElementById(targetId);
+    target?.scrollIntoView();
+
+  }, [isCommentsLoaded]);
+  // *******************************  end  *******************************
+
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
@@ -68,44 +110,43 @@ export const PageView = (props: Props): JSX.Element => {
     if (isNotCreatable) {
       return <NotCreatablePage />;
     }
-    if (isNotFound) {
-      return <NotFoundPage />;
-    }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound]);
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
   const sideContents = !isNotFound && !isNotCreatable
     ? (
       <PageSideContents page={page} />
     )
-    : <></>;
+    : null;
 
-  const footerContents = !isIdenticalPathPage && !isNotFound && page != null
+  const footerContents = !isIdenticalPathPage && !isNotFound
     ? (
       <>
-        { pageId != null && pagePath != null && (
-          <Comments pageId={pageId} pagePath={pagePath} revision={page.revision} />
-        ) }
-        { pagePath != null && isUsersHomePage(pagePath) && (
+        <div id="comments-container" ref={commentsContainerRef}>
+          <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} onLoaded={() => setCommentsLoaded(true)} />
+        </div>
+        { isUsersHomePagePath && (
           <UsersHomePageFooter creatorId={page.creator._id}/>
         ) }
         <PageContentFooter page={page} />
       </>
     )
-    : <></>;
+    : null;
 
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const Contents = () => {
+    if (isNotFound) {
+      return <NotFoundPage path={pagePath} />;
+    }
 
-  const contents = specialContents != null
-    ? <></>
-    // TODO: show SSR body
-    // : (() => {
-    //   const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
-    //     ssr: false,
-    //     // loading: () => ssrBody ?? <></>,
-    //   });
-    //   return <PageContents />;
-    // })();
-    : <PageContents />;
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const markdown = page.revision.body;
+
+    return (
+      <>
+        <PageContentsUtilities />
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      </>
+    );
+  };
 
   return (
     <MainPane
@@ -119,7 +160,7 @@ export const PageView = (props: Props): JSX.Element => {
         <>
           { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
-            { contents }
+            <Contents />
           </div>
         </>
       ) }

+ 22 - 58
packages/app/src/components/Page/RevisionLoader.tsx

@@ -1,11 +1,10 @@
-import React, { useEffect, useState, useCallback } from 'react';
+import React, { useState, useEffect } from 'react';
 
 import { Ref, IRevision, IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import { Waypoint } from 'react-waypoint';
 
-import { apiv3Get } from '~/client/util/apiv3-client';
 import { RendererOptions } from '~/services/renderer/renderer';
+import { useSWRxPageRevision } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from './RevisionRenderer';
@@ -16,7 +15,6 @@ export type RevisionLoaderProps = {
   rendererOptions: RendererOptions,
   pageId: string,
   revisionId: Ref<IRevision>,
-  lazy?: boolean,
   onRevisionLoaded?: (revision: IRevisionHasId) => void,
 }
 
@@ -34,61 +32,39 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    rendererOptions, pageId, revisionId, lazy, onRevisionLoaded,
+    rendererOptions, pageId, revisionId, onRevisionLoaded,
   } = props;
 
-  const [isLoading, setIsLoading] = useState<boolean>();
-  const [isLoaded, setIsLoaded] = useState<boolean>();
-  const [markdown, setMarkdown] = useState<string>('');
-  const [errors, setErrors] = useState<any | null>();
-
-  const loadData = useCallback(async() => {
-    if (!isLoaded && !isLoading) {
-      setIsLoading(true);
-    }
+  const { data: pageRevision, isLoading, error } = useSWRxPageRevision(pageId, revisionId);
 
-    // load data with REST API
-    try {
-      const res = await apiv3Get(`/revisions/${revisionId}`, { pageId });
+  const [markdown, setMarkdown] = useState<string>('');
 
-      setMarkdown(res.data?.revision?.body);
-      setErrors(null);
+  useEffect(() => {
+    if (pageRevision != null) {
+      setMarkdown(pageRevision?.body ?? '');
 
       if (onRevisionLoaded != null) {
-        onRevisionLoaded(res.data.revision);
+        onRevisionLoaded(pageRevision);
       }
     }
-    catch (errors) {
-      setErrors(errors);
-    }
-    finally {
-      setIsLoaded(true);
-      setIsLoading(false);
-    }
 
-  }, [isLoaded, isLoading, onRevisionLoaded, pageId, revisionId]);
+  }, [onRevisionLoaded, pageRevision]);
 
   useEffect(() => {
-    if (!lazy) {
-      loadData();
-    }
-  }, [lazy, loadData]);
-
-  const onWaypointChange = (event) => {
-    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
-      loadData();
+    if (error != null) {
+      const isForbidden = error != null && error[0].code === 'forbidden-page';
+      if (isForbidden) {
+        setMarkdown(`<i class="icon-exclamation p-1"></i>${t('not_allowed_to_see_this_page')}`);
+      }
+      else {
+        const errorMessages = error.map((error) => {
+          return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
+        });
+        setMarkdown(errorMessages.join('\n'));
+      }
     }
-    return;
-  };
+  }, [error, t]);
 
-  /* ----- before load ----- */
-  if (lazy && !isLoaded) {
-    return (
-      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px" />
-    );
-  }
-
-  /* ----- loading ----- */
   if (isLoading) {
     return (
       <div className="wiki">
@@ -99,18 +75,6 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
     );
   }
 
-  /* ----- after load ----- */
-  const isForbidden = errors != null && errors[0].code === 'forbidden-page';
-  if (isForbidden) {
-    setMarkdown(`<i class="icon-exclamation p-1"></i>${t('not_allowed_to_see_this_page')}`);
-  }
-  else if (errors != null) {
-    const errorMessages = errors.map((error) => {
-      return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
-    });
-    setMarkdown(errorMessages.join('\n'));
-  }
-
   return (
     <RevisionLoaderRoot>
       <RevisionRenderer

+ 28 - 3
packages/app/src/components/PageAccessoriesModal.tsx

@@ -17,7 +17,7 @@ import AttachmentIcon from './Icons/AttachmentIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import PageAttachment from './PageAttachment';
-import { PageHistory } from './PageHistory';
+import { PageHistory, getQueryParam } from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 
 import styles from './PageAccessoriesModal.module.scss';
@@ -27,6 +27,9 @@ const PageAccessoriesModal = (): JSX.Element => {
   const { t } = useTranslation();
 
   const [activeTab, setActiveTab] = useState<PageAccessoriesModalContents>(PageAccessoriesModalContents.PageHistory);
+  const [sourceRevisionId, setSourceRevisionId] = useState<string>();
+  const [targetRevisionId, setTargetRevisionId] = useState<string>();
+
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
   const { data: isSharedUser } = useIsSharedUser();
@@ -48,6 +51,28 @@ const PageAccessoriesModal = (): JSX.Element => {
     }, false);
   }, [mutate, status]);
 
+  // Set sourceRevisionId and targetRevisionId as state with valid object id string
+  useEffect(() => {
+    const queryParams = getQueryParam();
+    // https://regex101.com/r/YHTDsr/1
+    const regex = /^([0-9a-f]{24})...([0-9a-f]{24})$/i;
+
+    if (queryParams == null || !regex.test(queryParams)) {
+      return;
+    }
+
+    const matches = queryParams.match(regex);
+
+    if (matches == null) {
+      return;
+    }
+
+    const [, sourceRevisionId, targetRevisionId] = matches;
+    setSourceRevisionId(sourceRevisionId);
+    setTargetRevisionId(targetRevisionId);
+    mutate({ isOpened: true });
+  }, [mutate]);
+
   const navTabMapping = useMemo(() => {
     const isOpened = status == null ? false : status.isOpened;
     return {
@@ -57,7 +82,7 @@ const PageAccessoriesModal = (): JSX.Element => {
           if (!isOpened) {
             return <></>;
           }
-          return <PageHistory onClose={close}/>;
+          return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
         },
         i18n: t('History'),
         index: 0,
@@ -87,7 +112,7 @@ const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
       },
     };
-  }, [status, t, close, isGuestUser, isSharedUser, isLinkSharingDisabled]);
+  }, [status, t, close, sourceRevisionId, targetRevisionId, isGuestUser, isSharedUser, isLinkSharingDisabled]);
 
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">

+ 1 - 1
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -4,12 +4,12 @@ import dynamic from 'next/dynamic';
 
 import { useIsNotFound } from '~/stores/context';
 
-import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 
+const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 

+ 3 - 3
packages/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -18,21 +18,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
         return (
           <>
-            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')} only</strong>
+            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')}</strong>
           </>
         );
       }
       if (pageData.grant === 4) {
         return (
           <>
-            <i className="icon-fw icon-lock"></i><strong>{t('Only me')} only</strong>
+            <i className="icon-fw icon-lock"></i><strong>{t('Only me')}</strong>
           </>
         );
       }
       if (pageData.grant === 5) {
         return (
           <>
-            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name} only</strong>
+            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name}</strong>
           </>
         );
       }

+ 3 - 2
packages/app/src/components/PageComment/Comment.module.scss

@@ -1,3 +1,4 @@
+@use '../../styles/variables' as var;
 @use '../../styles/bootstrap/init' as bs;
 @use './_comment-inheritance';
 
@@ -10,10 +11,10 @@
 
   .page-comment {
     position: relative;
-    padding-top: 70px;
-    margin-top: -70px;
     pointer-events: none;
 
+    scroll-margin-top: var.$grw-scroll-margin-top-in-view;
+
     // user name
     .page-comment-creator {
       margin-top: -0.5em;

+ 7 - 5
packages/app/src/components/PageComment/Comment.tsx

@@ -80,17 +80,19 @@ export const Comment = (props: CommentProps): JSX.Element => {
     let className = 'page-comment flex-column';
 
     // TODO: fix so that `comment.createdAt` to be type Date https://redmine.weseek.co.jp/issues/113876
-    let commentCreatedAt = comment.createdAt;
-    if (typeof commentCreatedAt === 'string') {
-      commentCreatedAt = parseISO(commentCreatedAt);
-    }
+    const commentCreatedAtFixed = typeof comment.createdAt === 'string'
+      ? parseISO(comment.createdAt)
+      : comment.createdAt;
+    const revisionCreatedAtFixed = typeof revisionCreatedAt === 'string'
+      ? parseISO(revisionCreatedAt)
+      : revisionCreatedAt;
 
     // Conditional for called from SearchResultContext
     if (revisionId != null && revisionCreatedAt != null) {
       if (comment.revision === revisionId) {
         className += ' page-comment-current';
       }
-      else if (commentCreatedAt.getTime() > revisionCreatedAt.getTime()) {
+      else if (commentCreatedAtFixed.getTime() > revisionCreatedAtFixed.getTime()) {
         className += ' page-comment-newer';
       }
       else {

+ 11 - 6
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -85,12 +85,16 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setActiveTab(activeTab);
   }, []);
 
+  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  const slackChannelsDataString = slackChannelsData?.toString();
+  const initializeSlackEnabled = useCallback(() => {
+    setSlackChannels(slackChannelsDataString ?? '');
+    mutateIsSlackEnabled(false);
+  }, [mutateIsSlackEnabled, slackChannelsDataString]);
+
   useEffect(() => {
-    if (slackChannelsData != null) {
-      setSlackChannels(slackChannelsData.toString());
-      mutateIsSlackEnabled(false);
-    }
-  }, [mutateIsSlackEnabled, slackChannelsData]);
+    initializeSlackEnabled();
+  }, [initializeSlackEnabled]);
 
   const isSlackEnabledToggleHandler = (isSlackEnabled: boolean) => {
     mutateIsSlackEnabled(isSlackEnabled, false);
@@ -104,10 +108,11 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setComment('');
     setActiveTab('comment_editor');
     setError(undefined);
+    initializeSlackEnabled();
     // reset value
     if (editorRef.current == null) { return }
     editorRef.current.setValue('');
-  }, []);
+  }, [initializeSlackEnabled]);
 
   const cancelButtonClickedHandler = useCallback(() => {
     // change state to not ready

+ 8 - 0
packages/app/src/components/PageDeleteModal.tsx

@@ -83,6 +83,14 @@ const PageDeleteModal: FC = () => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [errs, setErrs] = useState<Error[] | null>(null);
 
+  // initialize when opening modal
+  useEffect(() => {
+    if (isOpened) {
+      setIsDeleteRecursively(true);
+      setIsDeleteCompletely(forceDeleteCompletelyMode);
+    }
+  }, [forceDeleteCompletelyMode, isOpened]);
+
   useEffect(() => {
     setIsDeleteCompletely(forceDeleteCompletelyMode);
   }, [forceDeleteCompletelyMode]);

+ 39 - 19
packages/app/src/components/PageEditor.tsx

@@ -7,7 +7,7 @@ import nodePath from 'path';
 
 
 import {
-  IPageHasId, PageGrant, pathUtils,
+  IPageHasId, pathUtils,
 } from '@growi/core';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
@@ -25,15 +25,22 @@ import {
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsNotFound, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
-  useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
+  useCurrentIndentSize, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
   useIsConflict,
   useEditingMarkdown,
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
-import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
+import {
+  useRemoteRevisionId,
+  useRemoteRevisionBody,
+  useRemoteRevisionLastUpdatedAt,
+  useRemoteRevisionLastUpdateUser,
+} from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -74,7 +81,8 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
@@ -90,7 +98,10 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
-  const { advance: advancePt } = usePageTreeTermManager();
+  const { mutate: mutateRemotePageId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -209,7 +220,7 @@ const PageEditor = React.memo((): JSX.Element => {
       );
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       return page;
     }
@@ -217,18 +228,18 @@ const PageEditor = React.memo((): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error);
       if (error.code === 'conflict') {
-        // pageContainer.setState({
-        //   remoteRevisionId: error.data.revisionId,
-        //   remoteRevisionBody: error.data.revisionBody,
-        //   remoteRevisionUpdateAt: error.data.createdAt,
-        //   lastUpdateUser: error.data.user,
-        // });
+        mutateRemotePageId(error.data.revisionId);
+        mutateRemoteRevisionId(error.data.revisionBody);
+        mutateRemoteRevisionLastUpdatedAt(error.data.createdAt);
+        mutateRemoteRevisionLastUpdateUser(error.data.user);
       }
       return null;
     }
 
-  // eslint-disable-next-line max-len
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId, advancePt]);
+  }, [
+    currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
+    currentPagePath, currentRevisionId, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
+  ]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {
@@ -255,11 +266,20 @@ const PageEditor = React.memo((): JSX.Element => {
     }
 
     const page = await save();
-    if (page != null) {
+    if (page == null) {
+      return;
+    }
+
+    if (isNotFound) {
+      await router.push(`/${page._id}#edit`);
+    }
+    else {
       updateStateAfterSave?.();
-      toastSuccess(t('toaster.save_succeeded'));
     }
-  }, [editorMode, save, t, updateStateAfterSave]);
+    toastSuccess(t('toaster.save_succeeded'));
+    mutateEditorMode(EditorMode.Editor);
+
+  }, [editorMode, isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
 
 
   /**

+ 9 - 8
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -11,10 +11,9 @@ import { throttle, debounce } from 'throttle-debounce';
 import urljoin from 'url-join';
 
 import InterceptorManager from '~/services/interceptor-manager';
-import { useHandsontableModal, useDrawioModal } from '~/stores/modal';
+import { useHandsontableModal, useDrawioModal, useTemplateModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
-import { TemplateModal } from '../TemplateModal';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 import AbstractEditor from './AbstractEditor';
@@ -873,7 +872,8 @@ class CodeMirrorEditor extends AbstractEditor {
   }
 
   showTemplateModal() {
-    this.setState({ isTemplateModalOpened: true });
+    const onSubmit = templateText => this.setValue(templateText);
+    this.props.onClickTemplateBtn(onSubmit);
   }
 
   // fold draw.io section (``` drawio ~ ```)
@@ -1158,11 +1158,6 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
-        <TemplateModal
-          isOpen={this.state.isTemplateModalOpened}
-          onClose={() => this.setState({ isTemplateModalOpened: false })}
-          onSubmit={templateText => this.setValue(templateText) }
-        />
       </div>
     );
   }
@@ -1187,6 +1182,7 @@ const CodeMirrorEditorMemoized = memo(CodeMirrorEditor);
 const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   const { open: openDrawioModal } = useDrawioModal();
   const { open: openHandsontableModal } = useHandsontableModal();
+  const { open: openTemplateModal } = useTemplateModal();
 
   const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
     openDrawioModal(drawioMxFile, onSave);
@@ -1196,11 +1192,16 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
     openHandsontableModal(markdownTable, editor, autoFormatMarkdownTable);
   }, [openHandsontableModal]);
 
+  const openTemplateModalHandler = useCallback((onSubmit) => {
+    openTemplateModal(onSubmit);
+  }, [openTemplateModal]);
+
   return (
     <CodeMirrorEditorMemoized
       ref={ref}
       onClickDrawioBtn={openDrawioModalHandler}
       onClickTableBtn={openTableModalHandler}
+      onClickTemplateBtn={openTemplateModalHandler}
       {...props}
     />
   );

+ 6 - 2
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import { UserPicture } from '@growi/ui';
 import CodeMirror from 'codemirror/lib/codemirror';
-import { format } from 'date-fns';
+import { format, parseISO } from 'date-fns';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -305,6 +305,10 @@ export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element =>
     return <></>;
   }
 
+  const currentPageCreatedAtFixed = typeof currentPage.updatedAt === 'string'
+    ? parseISO(currentPage.updatedAt)
+    : currentPage.updatedAt;
+
   const request: IRevisionOnConflictWithStringDate = {
     revisionId: '',
     revisionBody: props.markdownOnEdit,
@@ -314,7 +318,7 @@ export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element =>
   const origin: IRevisionOnConflictWithStringDate = {
     revisionId: currentPage?.revision._id,
     revisionBody: currentPage?.revision.body,
-    createdAt: format(currentPage.updatedAt, 'yyyy/MM/dd HH:mm:ss'),
+    createdAt: format(currentPageCreatedAtFixed, 'yyyy/MM/dd HH:mm:ss'),
     user: currentPage?.lastUpdateUser,
   };
   const latest: IRevisionOnConflictWithStringDate = {

+ 1 - 5
packages/app/src/components/PageEditor/Preview.tsx

@@ -3,7 +3,6 @@ import React, {
 } from 'react';
 
 import { RendererOptions } from '~/services/renderer/renderer';
-import { useEditorSettings } from '~/stores/editor';
 
 import RevisionRenderer from '../Page/RevisionRenderer';
 
@@ -22,12 +21,9 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
     markdown, pagePath,
   } = props;
 
-  const { data: editorSettings } = useEditorSettings();
-
-
   return (
     <div
-      className="page-editor-preview-body"
+      className={`page-editor-preview-body ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       ref={ref}
       onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
         if (props.onScroll != null) {

+ 11 - 10
packages/app/src/components/PageEditorByHackmd.tsx

@@ -24,8 +24,10 @@ import {
 import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   EditorMode,
@@ -64,12 +66,12 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const { returnPathForURL } = pathUtils;
 
   // pageData
-  const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
   const revision = pageData?.revision;
 
   const [isInitialized, setIsInitialized] = useState(false);
@@ -131,7 +133,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         mutateIsHackmdDraftUpdatingInRealtime(false);
 
         // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-        advancePt();
+        mutatePageTree();
       }
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
@@ -141,7 +143,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, advancePt]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
 
   // set handler to save and reload Page
   useEffect(() => {
@@ -264,7 +266,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutateTagsInfo();
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       mutateIsEnabledUnsavedWarning(false);
 
@@ -276,9 +278,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
-  }, [
-    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
-    saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, advancePt, mutateIsEnabledUnsavedWarning, t]);
+  // eslint-disable-next-line max-len
+  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
 
   /**
    * onChange event of HackmdEditor handler

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