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

Merge branch 'support/sharelink-ts-fc-swr' into support/sharelink-swr

keigo-h 3 лет назад
Родитель
Сommit
9d64643c4c
100 измененных файлов с 1309 добавлено и 989 удалено
  1. 1 1
      .devcontainer/devcontainer.json
  2. 5 2
      .github/workflows/reusable-app-prod.yml
  3. 12 14
      .vscode/settings.json
  4. 40 1
      CHANGELOG.md
  5. 1 1
      lerna.json
  6. 2 2
      package.json
  7. 1 0
      packages/app/_obsolete/config/webpack.common.js
  8. 0 0
      packages/app/_obsolete/src/client/boot.js
  9. 0 0
      packages/app/_obsolete/src/client/plugin.js
  10. 0 0
      packages/app/_obsolete/src/util/i18n.js
  11. 0 0
      packages/app/_obsolete/src/util/old-ios.js
  12. 1 0
      packages/app/config/ci/.env.local.for-ci
  13. 2 2
      packages/app/docker/README.md
  14. 5 22
      packages/app/next.config.js
  15. 20 15
      packages/app/package.json
  16. 7 5
      packages/app/public/static/locales/en_US/admin.json
  17. 2 0
      packages/app/public/static/locales/en_US/translation.json
  18. 6 4
      packages/app/public/static/locales/ja_JP/admin.json
  19. 2 0
      packages/app/public/static/locales/ja_JP/translation.json
  20. 7 4
      packages/app/public/static/locales/zh_CN/admin.json
  21. 2 0
      packages/app/public/static/locales/zh_CN/translation.json
  22. 1 0
      packages/app/regconfig.json
  23. 2 2
      packages/app/src/client/services/AppContainer.js
  24. 23 0
      packages/app/src/client/services/page-operation.ts
  25. 1 5
      packages/app/src/client/util/locale-utils.ts
  26. 18 2
      packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  27. 28 20
      packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx
  28. 8 11
      packages/app/src/components/Admin/AuditLogManagement.tsx
  29. 2 4
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  30. 2 4
      packages/app/src/components/Admin/Users/ExternalAccountTable.jsx
  31. 20 10
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  32. 3 0
      packages/app/src/components/CommonStyles/katex.module.scss
  33. 3 3
      packages/app/src/components/DescendantsPageList.tsx
  34. 1 7
      packages/app/src/components/DescendantsPageListModal.module.scss
  35. 20 4
      packages/app/src/components/DescendantsPageListModal.tsx
  36. 5 0
      packages/app/src/components/IdenticalPathPage.module.scss
  37. 4 1
      packages/app/src/components/IdenticalPathPage.tsx
  38. 23 31
      packages/app/src/components/InstallerForm.jsx
  39. 3 0
      packages/app/src/components/Layout/Admin.module.scss
  40. 3 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  41. 31 0
      packages/app/src/components/Navbar/AuthorInfo.module.scss
  42. 3 1
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  43. 1 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  44. 10 32
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  45. 13 5
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  46. 36 31
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  47. 10 2
      packages/app/src/components/Navbar/PageEditorModeManager.module.scss
  48. 71 6
      packages/app/src/components/Navbar/SubNavButtons.tsx
  49. 8 6
      packages/app/src/components/Page.jsx
  50. 5 0
      packages/app/src/components/Page/DisplaySwitcher.module.scss
  51. 6 8
      packages/app/src/components/Page/DisplaySwitcher.tsx
  52. 1 1
      packages/app/src/components/Page/RenderTagLabels.tsx
  53. 76 1
      packages/app/src/components/Page/RevisionRenderer.tsx
  54. 18 0
      packages/app/src/components/Page/TagLabels.module.scss
  55. 4 2
      packages/app/src/components/Page/TagLabels.tsx
  56. 18 0
      packages/app/src/components/PageAccessoriesModal.module.scss
  57. 2 1
      packages/app/src/components/PageAccessoriesModal.tsx
  58. 0 108
      packages/app/src/components/PageAccessoriesModalControl.jsx
  59. 2 4
      packages/app/src/components/PageAlert/PageGrantAlert.tsx
  60. 1 13
      packages/app/src/components/PageEditor.tsx
  61. 0 3
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  62. 1 1
      packages/app/src/components/PageEditor/DrawioModal.jsx
  63. 0 406
      packages/app/src/components/PageEditor/Editor.jsx
  64. 365 0
      packages/app/src/components/PageEditor/Editor.tsx
  65. 15 13
      packages/app/src/components/PageList/PageListItemL.tsx
  66. 3 3
      packages/app/src/components/PagePathHierarchicalLink.tsx
  67. 1 1
      packages/app/src/components/PagePresentationModal.jsx
  68. 0 4
      packages/app/src/components/PagePresentationModal.module.scss
  69. 14 0
      packages/app/src/components/PageTimeline.module.scss
  70. 10 3
      packages/app/src/components/PageTimeline.tsx
  71. 1 12
      packages/app/src/components/PrivateLegacyPages.tsx
  72. 22 0
      packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss
  73. 33 0
      packages/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  74. 2 2
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  75. 5 23
      packages/app/src/components/SearchPage.tsx
  76. 10 8
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  77. 0 1
      packages/app/src/components/SearchPage/SearchResultList.tsx
  78. 2 6
      packages/app/src/components/SearchPage2/SearchPageBase.tsx
  79. 50 29
      packages/app/src/components/ShareLink/ShareLink.tsx
  80. 3 3
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  81. 3 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  82. 1 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  83. 2 9
      packages/app/src/components/Skelton.tsx
  84. 11 0
      packages/app/src/components/TableOfContents.module.scss
  85. 19 13
      packages/app/src/components/TableOfContents.tsx
  86. 7 7
      packages/app/src/components/Theme/ThemeFuture.module.scss
  87. 8 0
      packages/app/src/components/Theme/ThemeFuture.tsx
  88. 7 7
      packages/app/src/components/Theme/ThemeHalloween.module.scss
  89. 8 0
      packages/app/src/components/Theme/ThemeHalloween.tsx
  90. 13 12
      packages/app/src/components/Theme/ThemeHufflepuff.module.scss
  91. 8 0
      packages/app/src/components/Theme/ThemeHufflepuff.tsx
  92. 7 7
      packages/app/src/components/Theme/ThemeKibela.module.scss
  93. 8 0
      packages/app/src/components/Theme/ThemeKibela.tsx
  94. 21 9
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  95. 17 0
      packages/app/src/interfaces/editor-methods.ts
  96. 15 4
      packages/app/src/interfaces/page-operation.ts
  97. 4 0
      packages/app/src/interfaces/services/renderer.ts
  98. 4 0
      packages/app/src/interfaces/share-link.ts
  99. 5 0
      packages/app/src/interfaces/theme.ts
  100. 2 1
      packages/app/src/linter-checker/test.scss

+ 1 - 1
.devcontainer/devcontainer.json

@@ -25,7 +25,7 @@
     "editorconfig.editorconfig",
     "editorconfig.editorconfig",
     "esbenp.prettier-vscode",
     "esbenp.prettier-vscode",
     "shinnn.stylelint",
     "shinnn.stylelint",
-    "hex-ci.stylelint-plus"
+    "stylelint.vscode-stylelint"
 	],
 	],
 
 
 	// Uncomment the next line if you want start specific services in your Docker Compose config.
 	// Uncomment the next line if you want start specific services in your Docker Compose config.

+ 5 - 2
.github/workflows/reusable-app-prod.yml

@@ -75,7 +75,7 @@ jobs:
     - name: Upload production files as artifact
     - name: Upload production files as artifact
       uses: actions/upload-artifact@v3
       uses: actions/upload-artifact@v3
       with:
       with:
-        name: Production Files
+        name: Production Files (node${{ inputs.node-version }})
         path: ${{ steps.archive-prod-files.outputs.file }}
         path: ${{ steps.archive-prod-files.outputs.file }}
 
 
     - name: Upload report as artifact
     - name: Upload report as artifact
@@ -155,7 +155,7 @@ jobs:
     - name: Download production files artifact
     - name: Download production files artifact
       uses: actions/download-artifact@v3
       uses: actions/download-artifact@v3
       with:
       with:
-        name: Production Files
+        name: Production Files (node${{ inputs.node-version }})
 
 
     - name: Extract procution files artifact
     - name: Extract procution files artifact
       run: |
       run: |
@@ -210,6 +210,9 @@ jobs:
     steps:
     steps:
     - uses: actions/checkout@v3
     - uses: actions/checkout@v3
 
 
+    - name: Install fonts
+      run: sudo apt install fonts-noto
+
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}

+ 12 - 14
.vscode/settings.json

@@ -3,23 +3,21 @@
 
 
   "eslint.workingDirectories": [{ "mode": "auto" }],
   "eslint.workingDirectories": [{ "mode": "auto" }],
 
 
-  // use stylelint-plus
-  // see https://qiita.com/y-w/items/bd7f11013fe34b69f0df#vs-code%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B
+  // use vscode-stylelint
+  // see https://github.com/stylelint/vscode-stylelint
+  "stylelint.validate": ["css", "less", "scss"],
+  "stylelint.ignoreDisables": true,
   "css.validate": false,
   "css.validate": false,
+  "less.validate": false,
   "scss.validate": false,
   "scss.validate": false,
-  "[css]": {
-    "editor.formatOnSave": true
-  },
-  "[scss]": {
-    "editor.formatOnSave": true
-  },
 
 
-  // for vscode-eslint
-  "[javascript]": {
-    "editor.formatOnSave": false
-  },
   "editor.codeActionsOnSave": {
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": true,
     "source.fixAll.eslint": true,
-    "source.fixAll.markdownlint": true
-  }
+    "source.fixAll.markdownlint": true,
+    "source.fixAll.stylelint": true
+  },
+
+  "githubPullRequests.ignoredPullRequestBranches": [
+    "master"
+  ]
 }
 }

+ 40 - 1
CHANGELOG.md

@@ -1,9 +1,48 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.1.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.2...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v5.1.2](https://github.com/weseek/growi/compare/v5.1.1...v5.1.2) - 2022-08-03
+
+### 💎 Features
+
+- feat: Make content width of each page configurable (#6107) @mudana-grune
+
+### 🚀 Improvement
+
+- imprv(auditlog): Clear and reload button (#6398) @miya
+- imprv(auditlog): Date Range Picker  (#6395) @miya
+
+### 🐛 Bug Fixes
+
+- fix: MathJax rendering (#6396) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Make Editor component Functional Component and TypeScript (#6374) @yukendev
+
+## [v5.1.1](https://github.com/weseek/growi/compare/v5.1.0...v5.1.1) - 2022-08-01
+
+### 💎 Features
+
+- feat: Users can set users per ip from env var at API Rate Limit  (#6379) @yukendev
+- feat: Show user picture in Audit Log (#6342) @miya
+- feat: Reset search criteria button (#6327) @miya
+
+### 🚀 Improvement
+
+- imprv(auditlog): Display number of actions that can be saved (#6353) @miya
+- imprv(auditlog): Include delete-related actions in small group (#6351) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Default markdown linker with relative path does not respect the current page path (v5.1.0) (#6378) @yuki-takei
+- fix: Recover page path operation (#6368) @hakumizuki
+- fix: Migration script for inserting NamedQuery (#6364) @yuki-takei
+- fix: "Error: cannnot get grant label" occured with lsx (#6348) @yukendev
+
 ## [v5.1.0](https://github.com/weseek/growi/compare/v5.0.11...v5.1.0) - 2022-07-21
 ## [v5.1.0](https://github.com/weseek/growi/compare/v5.0.11...v5.1.0) - 2022-07-21
 
 
 ### 💎 Features
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "5.1.1-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -76,7 +76,7 @@
     "reg-notify-github-plugin": "^0.11.1",
     "reg-notify-github-plugin": "^0.11.1",
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
-    "reg-suit": "^0.11.1",
+    "reg-suit": "^0.12.1",
     "rewire": "^5.0.0",
     "rewire": "^5.0.0",
     "shipjs": "^0.24.1",
     "shipjs": "^0.24.1",
     "stylelint": "^14.2.0",
     "stylelint": "^14.2.0",

+ 1 - 0
packages/app/_obsolete/config/webpack.common.js

@@ -77,6 +77,7 @@ module.exports = (options) => {
     },
     },
     node: {
     node: {
       fs: 'empty',
       fs: 'empty',
+      module: 'empty',
     },
     },
     module: {
     module: {
       rules: options.module.rules.concat([
       rules: options.module.rules.concat([

+ 0 - 0
packages/app/src/client/boot.js → packages/app/_obsolete/src/client/boot.js


+ 0 - 0
packages/app/src/client/plugin.js → packages/app/_obsolete/src/client/plugin.js


+ 0 - 0
packages/app/src/client/util/i18n.js → packages/app/_obsolete/src/util/i18n.js


+ 0 - 0
packages/app/src/client/util/old-ios.js → packages/app/_obsolete/src/util/old-ios.js


+ 1 - 0
packages/app/config/ci/.env.local.for-ci

@@ -1 +1,2 @@
 FORMAT_NODE_LOG=true
 FORMAT_NODE_LOG=true
+MATHJAX=1

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
-* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
+* [`5.1.2`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.2/docker/Dockerfile)
+* [`5.1.2-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.2/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/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)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 5 - 22
packages/app/next.config.js

@@ -9,12 +9,6 @@ const { withSuperjson } = require('next-superjson');
 const { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } = require('next/constants');
 const { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } = require('next/constants');
 
 
 
 
-// define additional entries
-const additionalWebpackEntries = {
-  boot: './src/client/boot',
-};
-
-
 const setupTranspileModules = () => {
 const setupTranspileModules = () => {
   const eazyLogger = require('eazy-logger');
   const eazyLogger = require('eazy-logger');
   const { listScopedPackages, listPrefixedPackages } = require('./src/utils/next.config.utils');
   const { listScopedPackages, listPrefixedPackages } = require('./src/utils/next.config.utils');
@@ -33,7 +27,9 @@ const setupTranspileModules = () => {
     'unified',
     'unified',
     'comma-separated-tokens',
     'comma-separated-tokens',
     'decode-named-character-reference',
     'decode-named-character-reference',
+    'hastscript',
     'html-void-elements',
     'html-void-elements',
+    'longest-streak',
     'property-information',
     'property-information',
     'space-separated-tokens',
     'space-separated-tokens',
     'trim-lines',
     'trim-lines',
@@ -41,7 +37,9 @@ const setupTranspileModules = () => {
     'vfile',
     'vfile',
     'zwitch',
     'zwitch',
     'emoticon',
     'emoticon',
-    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
+    'direction', // for hast-util-select
+    'bcp-47-match', // for hast-util-select
+    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
   ];
 
 
   logger.info('{bold:Listing scoped packages for transpiling:}');
   logger.info('{bold:Listing scoped packages for transpiling:}');
@@ -83,21 +81,6 @@ module.exports = async(phase, { defaultConfig }) => {
       config.externals.push('dtrace-provider');
       config.externals.push('dtrace-provider');
       config.externals.push('mongoose');
       config.externals.push('mongoose');
 
 
-      // configure additional entries
-      const orgEntry = config.entry;
-      config.entry = () => {
-        return orgEntry().then((entry) => {
-          return { ...entry, ...additionalWebpackEntries };
-        });
-      };
-
-      const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
-      config.plugins.push(
-        new WebpackManifestPlugin({
-          fileName: 'custom-manifest.json',
-        }),
-      );
-
       // setup i18next-hmr
       // setup i18next-hmr
       if (!options.isServer && options.dev) {
       if (!options.isServer && options.dev) {
         const { I18NextHMRPlugin } = require('i18next-hmr/plugin');
         const { I18NextHMRPlugin } = require('i18next-hmr/plugin');

+ 20 - 15
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "5.1.1-RC.0",
+  "version": "5.1.3-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -37,7 +37,7 @@
     "lint": "run-p lint:*",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
     "test:ci": "cross-env NODE_ENV=test jest",
     "test:ci": "cross-env NODE_ENV=test jest",
-    "prelint:eslint": "yarn resources:plugin",
+    "// prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "reg:run": "reg-suit run",
     "reg:run": "reg-suit run",
     "//// misc": "",
     "//// misc": "",
@@ -45,8 +45,9 @@
     "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
     "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
-    "resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
-    "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
+    "resources:dummy": "true",
+    "// resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
+    "// resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
@@ -57,17 +58,17 @@
   "dependencies": {
   "dependencies": {
     "@aws-sdk/client-s3": "^3.58.0",
     "@aws-sdk/client-s3": "^3.58.0",
     "@aws-sdk/s3-request-presigner": "^3.58.0",
     "@aws-sdk/s3-request-presigner": "^3.58.0",
-    "@browser-bunyan/console-formatted-stream": "^1.6.2",
+    "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch6": "npm:@elastic/elasticsearch@^6.8.8",
     "@elastic/elasticsearch6": "npm:@elastic/elasticsearch@^6.8.8",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.1-RC.0",
-    "@growi/core": "^5.1.1-RC.0",
-    "@growi/plugin-attachment-refs": "^5.1.1-RC.0",
-    "@growi/plugin-lsx": "^5.1.1-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.1-RC.0",
-    "@growi/slack": "^5.1.1-RC.0",
+    "@growi/codemirror-textlint": "^5.1.3-RC.0",
+    "@growi/core": "^5.1.3-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.3-RC.0",
+    "@growi/plugin-lsx": "^5.1.3-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.3-RC.0",
+    "@growi/slack": "^5.1.3-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -81,7 +82,7 @@
     "axios": "^0.24.0",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
     "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
     "body-parser": "^1.18.2",
-    "browser-bunyan": "^1.6.3",
+    "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.3",
     "bson-objectid": "^2.0.3",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
     "check-node-version": "^4.1.0",
     "check-node-version": "^4.1.0",
@@ -108,6 +109,7 @@
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
+    "hast-util-select": "^5.0.2",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
     "i18next-chained-backend": "^3.0.2",
     "i18next-chained-backend": "^3.0.2",
@@ -142,6 +144,7 @@
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-saml": "^3.2.0",
     "passport-twitter": "^1.0.4",
     "passport-twitter": "^1.0.4",
+    "prism-themes": "^1.9.0",
     "prom-client": "^13.0.0",
     "prom-client": "^13.0.0",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
@@ -153,8 +156,10 @@
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
     "react-markdown": "^8.0.3",
     "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
+    "react-syntax-highlighter": "^15.5.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
+    "rehype-katex": "^6.0.2",
     "rehype-raw": "^6.1.1",
     "rehype-raw": "^6.1.1",
     "rehype-sanitize": "^5.0.1",
     "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-slug": "^5.0.1",
@@ -162,6 +167,7 @@
     "remark-breaks": "^3.0.2",
     "remark-breaks": "^3.0.2",
     "remark-emoji": "^3.0.2",
     "remark-emoji": "^3.0.2",
     "remark-gfm": "^3.0.1",
     "remark-gfm": "^3.0.1",
+    "remark-math": "^5.1.1",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
@@ -184,7 +190,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.1-RC.0",
+    "@growi/ui": "^5.1.3-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",
@@ -247,7 +253,6 @@
     "ts-node": "^9.1.1",
     "ts-node": "^9.1.1",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9",
     "tsc-alias": "^1.2.9",
-    "unstated": "^2.1.1",
-    "webpack-manifest-plugin": "^5.0.0"
+    "unstated": "^2.1.1"
   }
   }
 }
 }

+ 7 - 5
packages/app/public/static/locales/en_US/admin.json

@@ -422,7 +422,7 @@
     }
     }
   },
   },
   "slack_integration_legacy": {
   "slack_integration_legacy": {
-    "alert_disabled": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
+    "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   },
   "user_management": {
   "user_management": {
@@ -537,15 +537,17 @@
     "url": "URL",
     "url": "URL",
     "settings": "Settings",
     "settings": "Settings",
     "return": "Return",
     "return": "Return",
-    "clear": "Clear search criteria",
-    "reload": "Reload",
+    "clear": "Clear",
     "activity_expiration_date": "Audit Log expiration date",
     "activity_expiration_date": "Audit Log expiration date",
     "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
     "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
     "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "available_action_list": "Search / View All Available Actions",
     "available_action_list": "Search / View All Available Actions",
-    "available_action_list_explain": "List of actions that can be search / view in the Audit Log",
+    "available_action_list_explain": "List of actions that can be searched/viewed in the current settings",
     "action_list": "Action List",
     "action_list": "Action List",
-    "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true."
+    "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true.",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
   },
   },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "Page",
     "Page": "Page",

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

@@ -168,6 +168,7 @@
   "No bookmarks yet": "No bookmarks yet",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
   "add_bookmark": "Add to Bookmarks",
   "remove_bookmark": "Remove from Bookmarks",
   "remove_bookmark": "Remove from Bookmarks",
+  "wide_view": "Wide View",
   "Recent Created": "Recent Created",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
   "Recent Changes": "Recent Changes",
   "Page Tree": "Page Tree",
   "Page Tree": "Page Tree",
@@ -545,6 +546,7 @@
     "create_failed": "Failed to create {{target}}",
     "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
     "update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_failed": "File upload failed.",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin",

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

@@ -536,15 +536,17 @@
     "url": "URL",
     "url": "URL",
     "settings": "設定",
     "settings": "設定",
     "return": "戻る",
     "return": "戻る",
-    "clear": "検索条件のクリア",
-    "reload": "再読み込み",
+    "clear": "クリア",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",
     "available_action_list": "検索 / 表示 可能なアクション一覧",
     "available_action_list": "検索 / 表示 可能なアクション一覧",
-    "available_action_list_explain": "監査ログで 検索 / 表示 可能なアクション一覧です",
+    "available_action_list_explain": "現在の設定で検索 / 表示 可能なアクション一覧です",
     "action_list": "アクション一覧",
     "action_list": "アクション一覧",
-    "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。"
+    "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
   },
   },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "ページ",
     "Page": "ページ",

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

@@ -170,6 +170,7 @@
   "No bookmarks yet": "No bookmarks yet",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "ブックマークに追加",
   "add_bookmark": "ブックマークに追加",
   "remove_bookmark": "ブックマークから削除",
   "remove_bookmark": "ブックマークから削除",
+  "wide_view": "ワイドビュー",
   "Recent Created": "最新の作成",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
   "Recent Changes": "最新の変更",
   "Page Tree": "ページツリー",
   "Page Tree": "ページツリー",
@@ -545,6 +546,7 @@
     "create_failed": "{{target}}の作成に失敗しました",
     "create_failed": "{{target}}の作成に失敗しました",
     "update_successed": "{{target}}を更新しました",
     "update_successed": "{{target}}を更新しました",
     "update_failed": "{{target}}の更新に失敗しました",
     "update_failed": "{{target}}の更新に失敗しました",
+    "file_upload_failed": "ファイルのアップロードに失敗しました",
     "initialize_successed": "{{target}}を初期化しました",
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "remove_user_admin": "{{username}}を管理者から外しました",
     "remove_user_admin": "{{username}}を管理者から外しました",

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

@@ -546,15 +546,18 @@
     "url": "URL",
     "url": "URL",
     "settings": "设置",
     "settings": "设置",
     "return": "返回",
     "return": "返回",
-    "clear": "清除搜索标准",
-    "reload": "重新加载",
+    "clear": "清除",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "available_action_list": "搜索/查看 所有可用的行动",
     "available_action_list": "搜索/查看 所有可用的行动",
-    "available_action_list_explain": "可以在审计日志中 搜索/查看 的行动列表",
+    "available_action_list_explain": "在当前配置中可以搜索/查看的行动列表",
     "action_list": "行动清单",
     "action_list": "行动清单",
-    "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。"
+    "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
+
   },
   },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "页面",
     "Page": "页面",

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

@@ -176,6 +176,7 @@
   "No bookmarks yet": "暂无书签",
   "No bookmarks yet": "暂无书签",
   "add_bookmark": "添加到书签",
   "add_bookmark": "添加到书签",
   "remove_bookmark": "从书签中删除",
   "remove_bookmark": "从书签中删除",
+  "wide_view": "视野开阔",
 	"Recent Created": "最新创建",
 	"Recent Created": "最新创建",
   "Recent Changes": "最新修改",
   "Recent Changes": "最新修改",
   "Page Tree": "页面树",
   "Page Tree": "页面树",
@@ -523,6 +524,7 @@
     "create_failed": "Failed to create {{target}}",
     "create_failed": "Failed to create {{target}}",
 		"update_successed": "Succeeded to update {{target}}",
 		"update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",
 		"give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",

+ 1 - 0
packages/app/regconfig.json

@@ -13,6 +13,7 @@
     "reg-notify-github-plugin": {
     "reg-notify-github-plugin": {
       "prCommentBehavior": "new",
       "prCommentBehavior": "new",
       "setCommitStatus": false,
       "setCommitStatus": false,
+      "shortDescription": true,
       "clientId": "$REG_NOTIFY_GITHUB_PLUGIN_CLIENTID"
       "clientId": "$REG_NOTIFY_GITHUB_PLUGIN_CLIENTID"
     },
     },
     "reg-notify-slack-plugin": {
     "reg-notify-slack-plugin": {

+ 2 - 2
packages/app/src/client/services/AppContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { i18nFactory } from '../util/i18n';
+// import { i18nFactory } from '../util/i18n';
 
 
 /**
 /**
  * Service container related to options for Application
  * Service container related to options for Application
@@ -20,7 +20,7 @@ export default class AppContainer extends Container {
       const currentUser = JSON.parse(currentUserElem.textContent);
       const currentUser = JSON.parse(currentUserElem.textContent);
       userLocaleId = currentUser?.lang;
       userLocaleId = currentUser?.lang;
     }
     }
-    this.i18n = i18nFactory(userLocaleId);
+    // this.i18n = i18nFactory(userLocaleId);
 
 
     this.containerInstances = {};
     this.containerInstances = {};
     this.componentInstances = {};
     this.componentInstances = {};

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

@@ -36,6 +36,29 @@ export const toggleBookmark = async(pageId: string, currentValue?: boolean): Pro
   }
   }
 };
 };
 
 
+// Utility to update body class
+const updateBodyClassByView = (expandContentWidth: boolean): void => {
+  const bodyClasses = document.body.classList;
+  const isLayoutFluid = bodyClasses.contains('growi-layout-fluid');
+
+  if (expandContentWidth && !isLayoutFluid) {
+    bodyClasses.add('growi-layout-fluid');
+  }
+  else if (isLayoutFluid) {
+    bodyClasses.remove('growi-layout-fluid');
+  }
+};
+
+export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
+  try {
+    await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
+    updateBodyClassByView(newValue);
+  }
+  catch (err) {
+    toastError(err);
+  }
+};
+
 export const bookmark = async(pageId: string): Promise<void> => {
 export const bookmark = async(pageId: string): Promise<void> => {
   try {
   try {
     await apiv3Put('/bookmarks', { pageId, bool: true });
     await apiv3Put('/bookmarks', { pageId, bool: true });

+ 1 - 5
packages/app/src/client/util/locale-utils.js → packages/app/src/client/util/locale-utils.ts

@@ -4,10 +4,6 @@ const DIAGRAMS_NET_LANG_MAP = {
   zh_CN: 'zh',
   zh_CN: 'zh',
 };
 };
 
 
-const getDiagramsNetLangCode = (lang) => {
+export const getDiagramsNetLangCode = (lang) => {
   return DIAGRAMS_NET_LANG_MAP[lang];
   return DIAGRAMS_NET_LANG_MAP[lang];
 };
 };
-
-module.exports = {
-  getDiagramsNetLangCode,
-};

+ 18 - 2
packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -3,6 +3,7 @@ import React, { FC, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
+import { AllSupportedActions } from '~/interfaces/activity';
 import { useActivityExpirationSeconds, useAuditLogAvailableActions } from '~/stores/context';
 import { useActivityExpirationSeconds, useAuditLogAvailableActions } from '~/stores/context';
 
 
 export const AuditLogSettings: FC = () => {
 export const AuditLogSettings: FC = () => {
@@ -34,8 +35,23 @@ export const AuditLogSettings: FC = () => {
         />
         />
       </p>
       </p>
 
 
-      <h4 className="mt-4">{t('admin:audit_log_management.available_action_list')}</h4>
-      <p className="form-text text-muted">{t('admin:audit_log_management.available_action_list_explain')}</p>
+      <h4 className="mt-4">
+        {t('admin:audit_log_management.available_action_list')}
+        <span className="badge badge-pill badge-secondary ml-2">
+          {`${availableActions.length} / ${AllSupportedActions.length}`}
+        </span>
+        <a
+          className="ml-2"
+          href={t('admin:audit_log_management.docs_url.log_type')}
+          target="_blank"
+          rel="noopener noreferrer"
+        >
+          <i className="icon-fw icon-question" aria-hidden="true"></i>
+        </a>
+      </h4>
+      <p className="form-text text-muted">
+        {t('admin:audit_log_management.available_action_list_explain')}
+      </p>
       <p className="mt-1">
       <p className="mt-1">
         <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
         <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
           <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>
           <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>

+ 28 - 20
packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,29 +1,39 @@
-import React, {
-  FC, useRef, forwardRef, useCallback,
-} from 'react';
+import React, { FC, forwardRef, useCallback } from 'react';
 
 
+import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 import 'react-datepicker/dist/react-datepicker.css';
 
 
-import { useTranslation } from 'react-i18next';
-
 
 
 type CustomInputProps = {
 type CustomInputProps = {
-  buttonRef: React.Ref<HTMLButtonElement>
-  onClick?: () => void;
+  value?: string
+  onChange?: () => void
+  onFocus?: () => void
 }
 }
 
 
-const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
-  const { t } = useTranslation();
+const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: CustomInputProps, ref) => {
+  const dateFormat = 'MM/dd/yyyy';
+  const date = new Date();
+  const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
+
   return (
   return (
-    <button
-      type="button"
-      className="btn btn-outline-secondary dropdown-toggle"
-      ref={props.buttonRef}
-      onClick={props.onClick}
-    >
-      <i className="fa fa-fw fa-calendar" /> {t('admin:audit_log_management.date')}
-    </button>
+    <div className="input-group admin-audit-log">
+      <div className="input-group-prepend">
+        <span className="input-group-text">
+          <i className="fa fa-fw fa-calendar" />
+        </span>
+      </div>
+      <input
+        ref={ref}
+        type="text"
+        value={props?.value}
+        onFocus={props?.onFocus}
+        onChange={props?.onChange}
+        placeholder={placeholder}
+        className="form-control date-range-picker"
+        aria-describedby="basic-addon1"
+      />
+    </div>
   );
   );
 });
 });
 
 
@@ -38,8 +48,6 @@ type DateRangePickerProps = {
 export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
 export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
   const { startDate, endDate, onChange } = props;
   const { startDate, endDate, onChange } = props;
 
 
-  const buttonRef = useRef(null);
-
   const changeHandler = useCallback((dateList: Date[] | null[]) => {
   const changeHandler = useCallback((dateList: Date[] | null[]) => {
     if (onChange != null) {
     if (onChange != null) {
       const [start, end] = dateList;
       const [start, end] = dateList;
@@ -60,7 +68,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePicker
         startDate={startDate}
         startDate={startDate}
         endDate={endDate}
         endDate={endDate}
         onChange={changeHandler}
         onChange={changeHandler}
-        customInput={<CustomInput buttonRef={buttonRef} />}
+        customInput={<CustomInput />}
       />
       />
     </div>
     </div>
   );
   );

+ 8 - 11
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -135,6 +135,11 @@ export const AuditLogManagement: FC = () => {
         <span>
         <span>
           {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
           {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
         </span>
         </span>
+        { !isSettingPage && (
+          <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+            <i className="icon icon-reload"></i>
+          </button>
+        )}
       </h2>
       </h2>
 
 
       {isSettingPage ? (
       {isSettingPage ? (
@@ -160,17 +165,9 @@ export const AuditLogManagement: FC = () => {
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
             />
             />
 
 
-            <div className="ml-auto">
-              <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={clearButtonPushedHandler}>
-                <span className="icon-refresh mr-1" />
-                {t('admin:audit_log_management.clear')}
-              </button>
-
-              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={reloadButtonPushedHandler}>
-                <i className="icon icon-reload mr-1" />
-                {t('admin:audit_log_management.reload')}
-              </button>
-            </div>
+            <button type="button" className="btn btn-link" onClick={clearButtonPushedHandler}>
+              {t('admin:audit_log_management.clear')}
+            </button>
           </div>
           </div>
 
 
           <p
           <p

+ 2 - 4
packages/app/src/components/Admin/ManageExternalAccount.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
@@ -79,7 +78,6 @@ class ManageExternalAccount extends React.Component {
 
 
 ManageExternalAccount.propTypes = {
 ManageExternalAccount.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
 };
 };
 
 
@@ -88,6 +86,6 @@ const ManageExternalAccountWrapperFC = (props) => {
   return <ManageExternalAccount t={t} {...props} />;
   return <ManageExternalAccount t={t} {...props} />;
 };
 };
 
 
-const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccountWrapperFC, [AppContainer, AdminExternalAccountsContainer]);
+const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccountWrapperFC, [AdminExternalAccountsContainer]);
 
 
 export default ManageExternalAccountWrapper;
 export default ManageExternalAccountWrapper;

+ 2 - 4
packages/app/src/components/Admin/Users/ExternalAccountTable.jsx

@@ -1,11 +1,10 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
 
 
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -119,7 +118,6 @@ class ExternalAccountTable extends React.Component {
 
 
 ExternalAccountTable.propTypes = {
 ExternalAccountTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
 };
 };
 
 
@@ -128,7 +126,7 @@ const ExternalAccountTableWrapperFC = (props) => {
   return <ExternalAccountTable t={t} {...props} />;
   return <ExternalAccountTable t={t} {...props} />;
 };
 };
 
 
-const ExternalAccountTableWrapper = withUnstatedContainers(ExternalAccountTableWrapperFC, [AppContainer, AdminExternalAccountsContainer]);
+const ExternalAccountTableWrapper = withUnstatedContainers(ExternalAccountTableWrapperFC, [AdminExternalAccountsContainer]);
 
 
 
 
 export default ExternalAccountTableWrapper;
 export default ExternalAccountTableWrapper;

+ 20 - 10
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -11,6 +11,7 @@ import {
 import { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
 
 
 const logger = loggerFactory('growi:cli:PageItemControl');
 const logger = loggerFactory('growi:cli:PageItemControl');
 
 
@@ -22,6 +23,7 @@ export const MenuItemType = {
   DELETE: 'delete',
   DELETE: 'delete',
   REVERT: 'revert',
   REVERT: 'revert',
   PATH_RECOVERY: 'pathRecovery',
   PATH_RECOVERY: 'pathRecovery',
+  SWITCH_CONTENT_WIDTH: 'switch_content_width',
 } as const;
 } as const;
 export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType];
 export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType];
 
 
@@ -41,6 +43,7 @@ type CommonProps = {
   onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
   onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
   onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void,
   onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void,
 
 
+  additionalMenuItemOnTopRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   isInstantRename?: boolean,
   isInstantRename?: boolean,
   alignRight?: boolean,
   alignRight?: boolean,
@@ -57,13 +60,14 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
   const {
   const {
-    pageId, isLoading,
-    pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickRevertMenuItem, onClickPathRecoveryMenuItem,
-    additionalMenuItemRenderer: AdditionalMenuItems, isInstantRename, alignRight,
+    pageId, isLoading, pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
+    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
+    onClickRevertMenuItem, onClickPathRecoveryMenuItem,
+    additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
+    additionalMenuItemRenderer: AdditionalMenuItems,
+    isInstantRename, alignRight,
   } = props;
   } = props;
 
 
-
   // eslint-disable-next-line react-hooks/rules-of-hooks
   // eslint-disable-next-line react-hooks/rules-of-hooks
   const bookmarkItemClickedHandler = useCallback(async() => {
   const bookmarkItemClickedHandler = useCallback(async() => {
     if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) {
     if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) {
@@ -136,7 +140,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
 
     // PathRecovery
     // PathRecovery
     // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
     // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
-    const shouldShowPathRecoveryButton = operationProcessData?.Rename != null ? operationProcessData?.Rename.isProcessable : false;
+    const shouldShowPathRecoveryButton = operationProcessData != null ? shouldRecoverPagePaths(operationProcessData) : false;
 
 
     contents = (
     contents = (
       <>
       <>
@@ -148,8 +152,15 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
           </DropdownItem>
           </DropdownItem>
         ) }
         ) }
 
 
+        { AdditionalMenuItemsOnTop && (
+          <>
+            <AdditionalMenuItemsOnTop pageInfo={pageInfo} />
+            <DropdownItem divider />
+          </>
+        ) }
+
         {/* Bookmark */}
         {/* Bookmark */}
-        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && !pageInfo.isEmpty && isIPageInfoForOperation(pageInfo) && (
+        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && isIPageInfoForOperation(pageInfo) && (
           <DropdownItem
           <DropdownItem
             onClick={bookmarkItemClickedHandler}
             onClick={bookmarkItemClickedHandler}
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
@@ -253,9 +264,8 @@ type PageItemControlSubstanceProps = CommonProps & {
 export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
 export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
 
 
   const {
   const {
-    pageId, pageInfo: presetPageInfo, fetchOnInit,
-    children,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
+    pageId, pageInfo: presetPageInfo, fetchOnInit, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
+    onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
   } = props;
   } = props;
 
 
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);

+ 3 - 0
packages/app/src/components/CommonStyles/katex.module.scss

@@ -0,0 +1,3 @@
+.katex-container :global {
+  @import '~katex/dist/katex.min';
+}

+ 3 - 3
packages/app/src/components/DescendantsPageList.tsx

@@ -103,7 +103,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
     );
     );
   }
   }
 
 
-  const showPager = pagingResult.items.length > pagingResult.limit;
+  const showPager = pagingResult.totalCount > pagingResult.limit;
 
 
   return (
   return (
     <>
     <>
@@ -130,11 +130,11 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
   );
   );
 };
 };
 
 
-type Props = {
+export type DescendantsPageListProps = {
   path: string,
   path: string,
 }
 }
 
 
-export const DescendantsPageList = (props: Props): JSX.Element => {
+export const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
   const { path } = props;
   const { path } = props;
 
 
   const [activePage, setActivePage] = useState(1);
   const [activePage, setActivePage] = useState(1);

+ 1 - 7
packages/app/src/styles/_page-accessories-modal.scss → packages/app/src/components/DescendantsPageListModal.module.scss

@@ -1,4 +1,4 @@
-.grw-page-accessories-modal {
+.grw-page-accessories-modal :global {
   .modal-header {
   .modal-header {
     button.close {
     button.close {
       margin: auto 0rem auto auto;
       margin: auto 0rem auto auto;
@@ -16,9 +16,3 @@
     margin-bottom: 0rem;
     margin-bottom: 0rem;
   }
   }
 }
 }
-
-// revision-history
-// to stay d2h-code-side-line-number in the revision history diff area
-.d2h-wrapper {
-  position: relative;
-}

+ 20 - 4
packages/app/src/components/DescendantsPageListModal.tsx

@@ -2,6 +2,7 @@
 import React, { useState, useMemo } from 'react';
 import React, { useState, useMemo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import {
 import {
   Modal, ModalHeader, ModalBody,
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
@@ -11,12 +12,22 @@ import { useDescendantsPageListModal } from '~/stores/modal';
 
 
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
-import { DescendantsPageList } from './DescendantsPageList';
+import { DescendantsPageListProps } from './DescendantsPageList';
 import ExpandOrContractButton from './ExpandOrContractButton';
 import ExpandOrContractButton from './ExpandOrContractButton';
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
-import { PageTimeline } from './PageTimeline';
 
 
+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 PageTimeline = (): JSX.Element => {
+  const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
+  return <PageTimeline />;
+};
 
 
 export const DescendantsPageListModal = (): JSX.Element => {
 export const DescendantsPageListModal = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -44,7 +55,12 @@ export const DescendantsPageListModal = (): JSX.Element => {
       },
       },
       timeline: {
       timeline: {
         Icon: TimeLineIcon,
         Icon: TimeLineIcon,
-        Content: () => <PageTimeline />,
+        Content: () => {
+          if (status == null || !status.isOpened) {
+            return <></>;
+          }
+          return <PageTimeline />;
+        },
         i18n: t('Timeline View'),
         i18n: t('Timeline View'),
         index: 1,
         index: 1,
         isLinkEnabled: () => !isSharedUser,
         isLinkEnabled: () => !isSharedUser,
@@ -78,7 +94,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
       isOpen={isOpened}
       isOpen={isOpened}
       toggle={close}
       toggle={close}
       data-testid="page-accessories-modal"
       data-testid="page-accessories-modal"
-      className={`grw-page-accessories-modal ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
+      className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>
       <ModalHeader className="p-0" toggle={close} close={buttons}>
         <CustomNavTab
         <CustomNavTab

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

@@ -0,0 +1,5 @@
+@use '~/styles/molecules/page-accessories-control';
+
+.grw-page-accessories-control :global {
+  @extend %grw-page-accessories-control;
+}

+ 4 - 1
packages/app/src/components/IdenticalPathPage.tsx

@@ -11,6 +11,9 @@ import PageListIcon from './Icons/PageListIcon';
 import { PageListItemL } from './PageList/PageListItemL';
 import { PageListItemL } from './PageList/PageListItemL';
 
 
 
 
+import styles from './IdenticalPathPage.module.scss';
+
+
 type IdenticalPathAlertProps = {
 type IdenticalPathAlertProps = {
   path? : string | null,
   path? : string | null,
 }
 }
@@ -67,7 +70,7 @@ export const IdenticalPathPage = (): JSX.Element => {
     <div className="d-flex flex-column flex-lg-row-reverse">
     <div className="d-flex flex-column flex-lg-row-reverse">
 
 
       <div className="grw-side-contents-container">
       <div className="grw-side-contents-container">
-        <div className="grw-page-accessories-control pb-1">
+        <div className={`pb-1 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
           { currentPath != null && !isSharedUser && (
           { currentPath != null && !isSharedUser && (
             <button
             <button
               type="button"
               type="button"

+ 23 - 31
packages/app/src/components/InstallerForm.jsx

@@ -1,10 +1,11 @@
 import React from 'react';
 import React from 'react';
 
 
 import i18next from 'i18next';
 import i18next from 'i18next';
-import { useTranslation } from 'next-i18next';
+import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-// import { localeMetadatas } from '~/client/util/i18n';
+import { i18n as i18nConfig } from '^/config/next-i18next.config';
+
 import { useCsrfToken } from '~/stores/context';
 import { useCsrfToken } from '~/stores/context';
 
 
 class InstallerForm extends React.Component {
 class InstallerForm extends React.Component {
@@ -15,21 +16,12 @@ class InstallerForm extends React.Component {
     this.state = {
     this.state = {
       isValidUserName: true,
       isValidUserName: true,
       isSubmittingDisabled: false,
       isSubmittingDisabled: false,
-      selectedLang: {},
     };
     };
     this.checkUserName = this.checkUserName.bind(this);
     this.checkUserName = this.checkUserName.bind(this);
 
 
     this.submitHandler = this.submitHandler.bind(this);
     this.submitHandler = this.submitHandler.bind(this);
   }
   }
 
 
-  // UNSAFE_componentWillMount() {
-  //   const meta = localeMetadatas.find(v => v.id === i18next.language);
-  //   if (meta == null) {
-  //     return this.setState({ selectedLang: localeMetadatas[0] });
-  //   }
-  //   this.setState({ selectedLang: meta });
-  // }
-
   checkUserName(event) {
   checkUserName(event) {
     const axios = require('axios').create({
     const axios = require('axios').create({
       headers: {
       headers: {
@@ -42,11 +34,6 @@ class InstallerForm extends React.Component {
       .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
       .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
   }
   }
 
 
-  changeLanguage(meta) {
-    i18next.changeLanguage(meta.id);
-    this.setState({ selectedLang: meta });
-  }
-
   submitHandler() {
   submitHandler() {
     if (this.state.isSubmittingDisabled) {
     if (this.state.isSubmittingDisabled) {
       return;
       return;
@@ -59,6 +46,7 @@ class InstallerForm extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { t } = this.props;
     const hasErrorClass = this.state.isValidUserName ? '' : ' has-error';
     const hasErrorClass = this.state.isValidUserName ? '' : ' has-error';
     const unavailableUserId = this.state.isValidUserName
     const unavailableUserId = this.state.isValidUserName
       ? ''
       ? ''
@@ -89,29 +77,33 @@ class InstallerForm extends React.Component {
                   aria-expanded="true"
                   aria-expanded="true"
                 >
                 >
                   <span className="float-left">
                   <span className="float-left">
-                    {this.state.selectedLang.displayName}
+                    {t('meta.display_name')}
                   </span>
                   </span>
                 </button>
                 </button>
                 <input
                 <input
                   type="hidden"
                   type="hidden"
-                  value={this.state.selectedLang.id}
                   name="registerForm[app:globalLang]"
                   name="registerForm[app:globalLang]"
                 />
                 />
-                {/* <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
+                <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                   {
-                    localeMetadatas.map(meta => (
-                      <button
-                        key={meta.id}
-                        data-testid={`dropdownLanguageMenu-${meta.id}`}
-                        className="dropdown-item"
-                        type="button"
-                        onClick={() => { this.changeLanguage(meta) }}
-                      >
-                        {meta.displayName}
-                      </button>
-                    ))
+                    i18nConfig.locales.map((locale) => {
+                      const fixedT = i18n.getFixedT(locale);
+                      i18n.loadLanguages(i18nConfig.locales);
+
+                      return (
+                        <button
+                          key={locale}
+                          data-testid={`dropdownLanguageMenu-${locale}`}
+                          className="dropdown-item"
+                          type="button"
+                          onClick={() => { i18next.changeLanguage(locale) }}
+                        >
+                          {fixedT('meta.display_name')}
+                        </button>
+                      );
+                    })
                   }
                   }
-                </div> */}
+                </div>
               </div>
               </div>
             </div>
             </div>
 
 

+ 3 - 0
packages/app/src/components/Layout/Admin.module.scss

@@ -227,6 +227,9 @@ $slack-work-space-name-card-border: #efc1f6;
       max-height: 500px;
       max-height: 500px;
       overflow-y: auto;
       overflow-y: auto;
     }
     }
+    .date-range-picker {
+      width: 188px;
+    }
   }
   }
 
 
   #layoutOptions {
   #layoutOptions {

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

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { localeMetadatas } from '~/client/util/i18n';
+// import { localeMetadatas } from '~/client/util/i18n';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePersonalSettings } from '~/stores/personal-settings';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -113,6 +113,7 @@ const BasicInfoSettings = (props: Props) => {
       <div className="form-group row">
       <div className="form-group row">
         <label className="text-left text-md-right col-md-3 col-form-label">{t('Language')}</label>
         <label className="text-left text-md-right col-md-3 col-form-label">{t('Language')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
+          {/*
           {
           {
             localeMetadatas.map(meta => (
             localeMetadatas.map(meta => (
               <div key={meta.id} className="custom-control custom-radio custom-control-inline">
               <div key={meta.id} className="custom-control custom-radio custom-control-inline">
@@ -128,6 +129,7 @@ const BasicInfoSettings = (props: Props) => {
               </div>
               </div>
             ))
             ))
           }
           }
+          */}
         </div>
         </div>
       </div>
       </div>
       <div className="form-group row">
       <div className="form-group row">

+ 31 - 0
packages/app/src/components/Navbar/AuthorInfo.module.scss

@@ -0,0 +1,31 @@
+@use '~/styles/bootstrap/init' as bs;
+
+$author-font-size: 12px;
+$date-font-size: 11px;
+
+.grw-author-info :global {
+  li {
+    font-size: $author-font-size;
+    list-style: none;
+  }
+
+  .text-date {
+    font-size: $date-font-size;
+  }
+
+  .picture {
+    width: 22px;
+    height: 22px;
+    border: 1px solid bs.$gray-300;
+
+    &.picture-xs {
+      width: 14px;
+      height: 14px;
+    }
+  }
+}
+
+.grw-author-info-skelton :global {
+  width: 139px;
+  height: calc((#{$author-font-size} + #{$date-font-size}) * #{bs.$line-height-base});
+}

+ 3 - 1
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -39,6 +39,8 @@ import { Skelton } from '../Skelton';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import { SubNavButtonsProps } from './SubNavButtons';
 import { SubNavButtonsProps } from './SubNavButtons';
 
 
+import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
+
 
 
 type AdditionalMenuItemsProps = {
 type AdditionalMenuItemsProps = {
   pageId: string,
   pageId: string,
@@ -155,7 +157,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const PageEditorModeManager = dynamic(
   const PageEditorModeManager = dynamic(
     () => import('./PageEditorModeManager'),
     () => import('./PageEditorModeManager'),
-    { ssr: false, loading: () => <Skelton width={213} height={33.99} /> },
+    { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
   );
   );
   const SubNavButtons = dynamic<SubNavButtonsProps>(
   const SubNavButtons = dynamic<SubNavButtonsProps>(
     () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
     () => import('./SubNavButtons').then(mod => mod.SubNavButtons),

+ 1 - 1
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -135,7 +135,7 @@ export const GrowiNavbar = (): JSX.Element => {
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
       {/* Brand Logo  */}
       {/* Brand Logo  */}
       <div className="navbar-brand mr-0">
       <div className="navbar-brand mr-0">
-        <Link href="/">
+        <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
           <a className="grw-logo d-block">
             <GrowiLogo />
             <GrowiLogo />
           </a>
           </a>

+ 10 - 32
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -88,42 +88,20 @@
       font-size: 16px;
       font-size: 16px;
     }
     }
 
 
-    ul.authors {
-      li {
-        font-size: 12px;
-        list-style: none;
-      }
+    .user-list-popover {
+      max-width: 200px;
 
 
-      .text-date {
-        font-size: 11px;
-      }
+      .user-list-content {
+        direction: rtl;
 
 
-      .picture {
-        width: 22px;
-        height: 22px;
-        border: 1px solid bs.$gray-300;
-
-        &.picture-xs {
-          width: 14px;
-          height: 14px;
+        .liker-user-count,
+        .seen-user-count {
+          font-size: 12px;
+          font-weight: bolder;
         }
         }
       }
       }
-
-      .user-list-popover {
-        max-width: 200px;
-
-        .user-list-content {
-          direction: rtl;
-
-          .liker-user-count,
-          .seen-user-count {
-            font-size: 12px;
-            font-weight: bolder;
-          }
-        }
-        .cls-1 {
-          isolation: isolate;
-        }
+      .cls-1 {
+        isolation: isolate;
       }
       }
     }
     }
   }
   }

+ 13 - 5
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -14,10 +14,12 @@ import { Skelton } from '../Skelton';
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
 
 
 
 
+import TagLabelsStyles from '../Page/TagLabels.module.scss';
+import AuthorInfoStyles from './AuthorInfo.module.scss';
 import styles from './GrowiSubNavigation.module.scss';
 import styles from './GrowiSubNavigation.module.scss';
 
 
 
 
-type Props = {
+export type GrowiSubNavigationProps = {
   page: Partial<IPageHasId>,
   page: Partial<IPageHasId>,
 
 
   showDrawerToggler?: boolean,
   showDrawerToggler?: boolean,
@@ -35,10 +37,16 @@ type Props = {
   additionalClasses?: string[],
   additionalClasses?: string[],
 }
 }
 
 
-export const GrowiSubNavigation = (props: Props): JSX.Element => {
+export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element => {
 
 
-  const TagLabels = dynamic(() => import('../Page/TagLabels'), { ssr: false, loading: () => <Skelton width={137} height={21.99} additionalClass='py-1' /> });
-  const AuthorInfo = dynamic(() => import('./AuthorInfo'), { ssr: false, loading: () => <Skelton width={139} height={32.84} additionalClass='py-1' /> });
+  const TagLabels = dynamic(() => import('../Page/TagLabels'), {
+    ssr: false,
+    loading: () => <Skelton additionalClass={`${TagLabelsStyles['grw-tag-labels-skelton']} py-1`} />,
+  });
+  const AuthorInfo = dynamic(() => import('./AuthorInfo'), {
+    ssr: false,
+    loading: () => <Skelton additionalClass={`${AuthorInfoStyles['grw-author-info-skelton']} py-1`} />,
+  });
 
 
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
 
 
@@ -95,7 +103,7 @@ export const GrowiSubNavigation = (props: Props): JSX.Element => {
 
 
         {/* Page Authors */}
         {/* Page Authors */}
         { (showPageAuthors && !isCompactMode) && (
         { (showPageAuthors && !isCompactMode) && (
-          <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3">
+          <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
             <li className="pb-1">
             <li className="pb-1">
               <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
               <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
             </li>
             </li>

+ 36 - 31
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -33,7 +33,12 @@ const GrowiSubNavigationSwitcher = (props) => {
   const [width, setWidth] = useState(null);
   const [width, setWidth] = useState(null);
 
 
   const fixedContainerRef = useRef();
   const fixedContainerRef = useRef();
-  const stickyEvents = useMemo(() => new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' }), []);
+  /*
+  * Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
+  * The above err occurs when moving to admin page after rendering normal pages.
+  * This is because id "grw-subnav-sticky-trigger" does not exist on admin pages.
+  */
+  // const stickyEvents = useMemo(() => new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' }), []);
 
 
   const initWidth = useCallback(() => {
   const initWidth = useCallback(() => {
     const instance = fixedContainerRef.current;
     const instance = fixedContainerRef.current;
@@ -48,18 +53,18 @@ const GrowiSubNavigationSwitcher = (props) => {
     setWidth(clientWidth);
     setWidth(clientWidth);
   }, []);
   }, []);
 
 
-  const initVisible = useCallback(() => {
-    const elements = stickyEvents.stickyElements;
+  // const initVisible = useCallback(() => {
+  //   const elements = stickyEvents.stickyElements;
 
 
-    for (const elem of elements) {
-      const bool = stickyEvents.isSticking(elem);
-      if (bool) {
-        setVisible(bool);
-        break;
-      }
-    }
+  //   for (const elem of elements) {
+  //     const bool = stickyEvents.isSticking(elem);
+  //     if (bool) {
+  //       setVisible(bool);
+  //       break;
+  //     }
+  //   }
 
 
-  }, [stickyEvents]);
+  // }, [stickyEvents]);
 
 
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {
@@ -78,19 +83,19 @@ const GrowiSubNavigationSwitcher = (props) => {
     setVisible(event.detail.isSticky);
     setVisible(event.detail.isSticky);
   }, []);
   }, []);
 
 
-  // setup effect by sticky event
-  useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+  // // setup effect by sticky event
+  // useEffect(() => {
+  //   // sticky
+  //   // See: https://github.com/ryanwalters/sticky-events
+  //   const { stickySelector } = stickyEvents;
+  //   const elem = document.querySelector(stickySelector);
+  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
 
 
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler, stickyEvents]);
+  //   // return clean up handler
+  //   return () => {
+  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+  //   };
+  // }, [stickyChangeHandler, stickyEvents]);
 
 
   // update width when sidebar collapsing changed
   // update width when sidebar collapsing changed
   useEffect(() => {
   useEffect(() => {
@@ -99,16 +104,16 @@ const GrowiSubNavigationSwitcher = (props) => {
     }
     }
   }, [isSidebarCollapsed, initWidth]);
   }, [isSidebarCollapsed, initWidth]);
 
 
-  // initialize
-  useEffect(() => {
-    initWidth();
+  // // initialize
+  // useEffect(() => {
+  //   initWidth();
 
 
-    // check sticky state several times
-    setTimeout(initVisible, 100);
-    setTimeout(initVisible, 300);
-    setTimeout(initVisible, 2000);
+  //   // check sticky state several times
+  //   setTimeout(initVisible, 100);
+  //   setTimeout(initVisible, 300);
+  //   setTimeout(initVisible, 2000);
 
 
-  }, [initWidth, initVisible]);
+  // }, [initWidth, initVisible]);
 
 
   // ${styles['grw-subnav-switcher']}
   // ${styles['grw-subnav-switcher']}
 
 

+ 10 - 2
packages/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -2,6 +2,8 @@
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/mixins';
 @use '~/styles/mixins';
 
 
+$btn-line-height: 1.2rem;
+
 .grw-page-editor-mode-manager :global {
 .grw-page-editor-mode-manager :global {
   .btn {
   .btn {
     width: 70px;
     width: 70px;
@@ -11,7 +13,7 @@
 
 
     &.view-button,
     &.view-button,
     &.edit-button {
     &.edit-button {
-      line-height: 1.2rem;
+      line-height: $btn-line-height;
       .grw-page-editor-mode-manager-icon {
       .grw-page-editor-mode-manager-icon {
         @include bs.media-breakpoint-down(sm) {
         @include bs.media-breakpoint-down(sm) {
           font-size: 1.2rem;
           font-size: 1.2rem;
@@ -19,7 +21,7 @@
       }
       }
     }
     }
     &.hackmd-button {
     &.hackmd-button {
-      line-height: 1.2rem;
+      line-height: $btn-line-height;
       .grw-page-editor-mode-manager-icon {
       .grw-page-editor-mode-manager-icon {
         @include bs.media-breakpoint-down(sm) {
         @include bs.media-breakpoint-down(sm) {
           font-size: 1.2rem;
           font-size: 1.2rem;
@@ -32,3 +34,9 @@
     }
     }
   }
   }
 }
 }
+
+.grw-page-editor-mode-manager-skelton :global {
+
+  width: 213px;
+  height: calc($btn-line-height + bs.$btn-padding-y*2 + bs.$btn-border-width*2);
+}

+ 71 - 6
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -1,10 +1,14 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
 
 
-import dynamic from 'next/dynamic';
+import { useTranslation } from 'next-i18next';
+import { DropdownItem } from 'reactstrap';
 
 
-import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
 import {
 import {
-  IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
+  toggleBookmark, toggleLike, toggleSubscribe, updateContentWidth,
+} from '~/client/services/page-operation';
+import { toastError } from '~/client/util/apiNotification';
+import {
+  IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { useIsGuestUser } from '~/stores/context';
 import { useIsGuestUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
@@ -22,6 +26,43 @@ import SubscribeButton from '../SubscribeButton';
 import SeenUserInfo from '../User/SeenUserInfo';
 import SeenUserInfo from '../User/SeenUserInfo';
 
 
 
 
+type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
+  onClickMenuItem: (newValue: boolean) => void,
+}
+
+const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    pageInfo, onClickMenuItem,
+  } = props;
+
+  if (!isIPageInfoForEntity(pageInfo)) {
+    return <></>;
+  }
+
+  return (
+    <DropdownItem
+      onClick={() => onClickMenuItem(!pageInfo.expandContentWidth)}
+      className="grw-page-control-dropdown-item"
+    >
+      <div className="custom-control custom-switch ml-1">
+        <input
+          id="switchContentWidth"
+          className="custom-control-input"
+          type="checkbox"
+          checked={pageInfo.expandContentWidth}
+          onChange={() => {}}
+        />
+        <label className="custom-control-label" htmlFor="switchContentWidth">
+          { t('wide_view') }
+        </label>
+      </div>
+    </DropdownItem>
+  );
+};
+
+
 type CommonProps = {
 type CommonProps = {
   isCompactMode?: boolean,
   isCompactMode?: boolean,
   disableSeenUserInfoPopover?: boolean,
   disableSeenUserInfoPopover?: boolean,
@@ -38,7 +79,7 @@ type SubNavButtonsSubstanceProps = CommonProps & {
   shareLinkId?: string | null,
   shareLinkId?: string | null,
   revisionId: string | null,
   revisionId: string | null,
   path?: string | null,
   path?: string | null,
-  pageInfo: IPageInfoAll,
+  pageInfo: IPageInfoForOperation,
 }
 }
 
 
 const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element => {
 const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element => {
@@ -143,11 +184,34 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     onClickDeleteMenuItem(pageToDelete);
     onClickDeleteMenuItem(pageToDelete);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
 
+  const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
+    if (isGuestUser == null || isGuestUser) {
+      return;
+    }
+    if (!isIPageInfoForEntity(pageInfo)) {
+      return;
+    }
+    try {
+      await updateContentWidth(pageId, newValue);
+      mutatePageInfo();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
+
+  const additionalMenuItemOnTopRenderer = useMemo(() => {
+    if (!isIPageInfoForEntity(pageInfo)) {
+      return undefined;
+    }
+    const wideviewMenuItemRenderer = (props: WideViewMenuItemProps) => <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} />;
+    return wideviewMenuItemRenderer;
+  }, [pageInfo, switchContentWidthClickHandler]);
+
   if (!isIPageInfoForOperation(pageInfo)) {
   if (!isIPageInfoForOperation(pageInfo)) {
     return <></>;
     return <></>;
   }
   }
 
 
-
   const {
   const {
     sumOfLikers, sumOfSeenUsers, isLiked, bookmarkCount, isBookmarked,
     sumOfLikers, sumOfSeenUsers, isLiked, bookmarkCount, isBookmarked,
   } = pageInfo;
   } = pageInfo;
@@ -195,6 +259,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageInfo={pageInfo}
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
           isEnableActions={!isGuestUser}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
+          additionalMenuItemOnTopRenderer={additionalMenuItemOnTopRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickRenameMenuItem={renameMenuItemClickHandler}
           onClickRenameMenuItem={renameMenuItemClickHandler}
           onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
           onClickDuplicateMenuItem={duplicateMenuItemClickHandler}

+ 8 - 6
packages/app/src/components/Page.jsx

@@ -1,14 +1,13 @@
 import React, {
 import React, {
-  useCallback, useEffect, useMemo, useRef, useState,
+  useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import { debounce } from 'throttle-debounce';
+// import { debounce } from 'throttle-debounce';
 
 
-import MarkdownTable from '~/client/models/MarkdownTable';
 import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
-import { getOptionsToSave } from '~/client/util/editor';
+// import { getOptionsToSave } from '~/client/util/editor';
 import {
 import {
   useIsGuestUser, useIsBlinkedHeaderAtBoot,
   useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -23,8 +22,11 @@ import {
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import RevisionRenderer from './Page/RevisionRenderer';
 import RevisionRenderer from './Page/RevisionRenderer';
-import mdu from './PageEditor/MarkdownDrawioUtil';
-import mtu from './PageEditor/MarkdownTableUtil';
+
+// TODO: import dynamically
+// import MarkdownTable from '~/client/models/MarkdownTable';
+// import mdu from './PageEditor/MarkdownDrawioUtil';
+// import mtu from './PageEditor/MarkdownTableUtil';
 
 
 const logger = loggerFactory('growi:Page');
 const logger = loggerFactory('growi:Page');
 
 

+ 5 - 0
packages/app/src/components/Page/DisplaySwitcher.module.scss

@@ -0,0 +1,5 @@
+@use '~/styles/molecules/page-accessories-control';
+
+.grw-page-accessories-control :global {
+  @extend %grw-page-accessories-control;
+}

+ 6 - 8
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { TabContent, TabPane } from 'reactstrap';
 import { TabContent, TabPane } from 'reactstrap';
 
 
-import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
+// import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
   useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -15,13 +15,13 @@ import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import CountBadge from '../Common/CountBadge';
 import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
 import PageListIcon from '../Icons/PageListIcon';
-import NotFoundPage from '../NotFoundPage';
 import { Page } from '../Page';
 import { Page } from '../Page';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
 import TableOfContents from '../TableOfContents';
 import UserInfo from '../User/UserInfo';
 import UserInfo from '../User/UserInfo';
 
 
-import styles from '../TableOfContents.module.scss';
+
+import styles from './DisplaySwitcher.module.scss';
 
 
 
 
 const WIKI_HEADER_LINK = 120;
 const WIKI_HEADER_LINK = 120;
@@ -77,7 +77,7 @@ const DisplaySwitcher = (): JSX.Element => {
                 <div className="grw-side-contents-sticky-container">
                 <div className="grw-side-contents-sticky-container">
 
 
                   {/* Page list */}
                   {/* Page list */}
-                  <div className="grw-page-accessories-control">
+                  <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
                     { currentPagePath != null && !isSharedUser && (
                     { currentPagePath != null && !isSharedUser && (
                       <button
                       <button
                         type="button"
                         type="button"
@@ -97,7 +97,7 @@ const DisplaySwitcher = (): JSX.Element => {
                   {/* Comments */}
                   {/* Comments */}
                   {/* { getCommentListDom != null && !isTopPagePath && ( */}
                   {/* { getCommentListDom != null && !isTopPagePath && ( */}
                   { !isTopPagePath && (
                   { !isTopPagePath && (
-                    <div className="grw-page-accessories-control mt-2">
+                    <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
                       <button
                       <button
                         type="button"
                         type="button"
                         className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
                         className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
@@ -111,9 +111,7 @@ const DisplaySwitcher = (): JSX.Element => {
                   ) }
                   ) }
 
 
                   <div className="d-none d-lg-block">
                   <div className="d-none d-lg-block">
-                    <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>
-                      <TableOfContents />
-                    </div>
+                    <TableOfContents />
                     <ContentLinkButtons />
                     <ContentLinkButtons />
                   </div>
                   </div>
 
 

+ 1 - 1
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -35,7 +35,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
 
 
       <div id="edit-tags-btn-wrapper-for-tooltip">
       <div id="edit-tags-btn-wrapper-for-tooltip">
         <a
         <a
-          className={`btn btn-link btn-edit-tags p-0 text-muted ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          className={`btn btn-link btn-edit-tags p-0 text-muted d-flex ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
           onClick={openEditorHandler}
           onClick={openEditorHandler}
         >
         >
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}

+ 76 - 1
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -13,6 +13,8 @@ import loggerFactory from '~/utils/logger';
 
 
 // import RevisionBody from './RevisionBody';
 // import RevisionBody from './RevisionBody';
 
 
+import katexStyles from '../CommonStyles/katex.module.scss';
+
 
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 
@@ -100,7 +102,10 @@ const RevisionRenderer = (props: Props): JSX.Element => {
   } = props;
   } = props;
 
 
   return (
   return (
-    <ReactMarkdown {...rendererOptions} className={`wiki ${additionalClassName ?? ''}`}>
+    <ReactMarkdown
+      {...rendererOptions}
+      className={`wiki katex-container ${katexStyles['katex-container']} ${additionalClassName ?? ''}`}
+    >
       {markdown}
       {markdown}
     </ReactMarkdown>
     </ReactMarkdown>
   );
   );
@@ -122,6 +127,76 @@ const RevisionRenderer = (props: Props): JSX.Element => {
   // }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
   // }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
 
 
 
 
+  // const renderHtml = useCallback(async() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   const context = currentRenderingContext;
+
+  //   await interceptorManager.process('preRender', context);
+  //   await interceptorManager.process('prePreProcess', context);
+  //   context.markdown = growiRenderer.preProcess(context.markdown, context);
+  //   await interceptorManager.process('postPreProcess', context);
+  //   context.parsedHTML = growiRenderer.process(context.markdown, context);
+  //   await interceptorManager.process('prePostProcess', context);
+  //   context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+
+  //   const isMarkdownEmpty = context.markdown.trim().length === 0;
+  //   if (highlightKeywords != null && !isMarkdownEmpty) {
+  //     context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
+  //   }
+  //   await interceptorManager.process('postPostProcess', context);
+  //   await interceptorManager.process('preRenderHtml', context);
+
+  //   setHtml(context.parsedHTML);
+  // }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
+
+  // useEffect(() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   renderHtml()
+  //     .then(() => {
+  //       // const HeaderLink = document.getElementsByClassName('revision-head-link');
+  //       // const HeaderLinkArray = Array.from(HeaderLink);
+  //       // addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
+
+  //       // interceptorManager.process('postRenderHtml', currentRenderingContext);
+  //     });
+
+  // }, [currentRenderingContext, interceptorManager, renderHtml]);
+
+  // const config = props.appContainer.getConfig();
+  // const isMathJaxEnabled = !!config.env.MATHJAX;
+
+  // return (
+  //   <RevisionBody
+  //     html={html}
+  //     isMathJaxEnabled={isMathJaxEnabled}
+  //     additionalClassName={props.additionalClassName}
+  //     renderMathJaxOnInit
+  //   />
+  // );
+
+  // const [html, setHtml] = useState('');
+
+  // const { data: interceptorManager } = useInterceptorManager();
+  // const { data: editorSettings } = useEditorSettings();
+  // const { data: currentPathname } = useCurrentPathname();
+
+  // const currentRenderingContext = useMemo(() => {
+  //   return {
+  //     markdown,
+  //     parsedHTML: '',
+  //     pagePath,
+  //     renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+  //     currentPathname: decodeURIComponent(currentPathname ?? '/'),
+  //   };
+  // }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
+
+
   // const renderHtml = useCallback(async() => {
   // const renderHtml = useCallback(async() => {
   //   if (interceptorManager == null) {
   //   if (interceptorManager == null) {
   //     return;
   //     return;

+ 18 - 0
packages/app/src/components/Page/TagLabels.module.scss

@@ -0,0 +1,18 @@
+@use '~/styles/bootstrap/init' as bs;
+
+$grw-tag-label-font-size: 12px;
+
+.grw-tag-labels :global {
+  .grw-tag-label {
+    font-size: $grw-tag-label-font-size;
+    font-weight: normal;
+    border-radius: bs.$border-radius;
+  }
+}
+
+
+.grw-tag-labels-skelton :global {
+  width: 137px;
+  height: calc(#{$grw-tag-label-font-size} + #{bs.$badge-padding-y} * 2);
+  font-size: $grw-tag-label-font-size; // set font-size to use the same em value in bs.$badge-padding-y(https://getbootstrap.jp/docs/5.0/components/badge/#variables)
+}

+ 4 - 2
packages/app/src/components/Page/TagLabels.tsx

@@ -3,6 +3,8 @@ import React, { FC, useState } from 'react';
 import RenderTagLabels from './RenderTagLabels';
 import RenderTagLabels from './RenderTagLabels';
 import TagEditModal from './TagEditModal';
 import TagEditModal from './TagEditModal';
 
 
+import styles from './TagLabels.module.scss';
+
 type Props = {
 type Props = {
   tags?: string[],
   tags?: string[],
   isGuestUser: boolean,
   isGuestUser: boolean,
@@ -25,7 +27,7 @@ const TagLabels:FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <>
     <>
-      <form className="grw-tag-labels form-inline">
+      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`}>
         <i className="tag-icon icon-tag mr-2"></i>
         <i className="tag-icon icon-tag mr-2"></i>
         { tags == null
         { tags == null
           ? (
           ? (
@@ -39,7 +41,7 @@ const TagLabels:FC<Props> = (props: Props) => {
             />
             />
           )
           )
         }
         }
-      </form>
+      </div>
 
 
       <TagEditModal
       <TagEditModal
         tags={tags}
         tags={tags}

+ 18 - 0
packages/app/src/components/PageAccessoriesModal.module.scss

@@ -0,0 +1,18 @@
+.grw-page-accessories-modal :global {
+  .modal-header {
+    button.close {
+      margin: auto 0rem auto auto;
+    }
+  }
+
+  .modal-body {
+    padding: 25px 30px;
+  }
+
+  .grw-modal-body-style {
+    max-height: calc(100vh - 100px);
+  }
+  ul.pagination {
+    margin-bottom: 0rem;
+  }
+}

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

@@ -21,6 +21,7 @@ import PageHistory from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 import ShareLink from './ShareLink/ShareLink';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
+import styles from './PageAccessoriesModal.module.scss';
 
 
 type Props = {
 type Props = {
   appContainer: AppContainer,
   appContainer: AppContainer,
@@ -107,7 +108,7 @@ const PageAccessoriesModal = (props: Props): JSX.Element => {
       isOpen={isOpened}
       isOpen={isOpened}
       toggle={close}
       toggle={close}
       data-testid="page-accessories-modal"
       data-testid="page-accessories-modal"
-      className={`grw-page-accessories-modal ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
+      className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>
       <ModalHeader className="p-0" toggle={close} close={buttons}>
         <CustomNavTab
         <CustomNavTab

+ 0 - 108
packages/app/src/components/PageAccessoriesModalControl.jsx

@@ -1,108 +0,0 @@
-import React, { Fragment, useMemo } from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useCurrentPageId } from '~/stores/context';
-
-import AttachmentIcon from './Icons/AttachmentIcon';
-import HistoryIcon from './Icons/HistoryIcon';
-import PageListIcon from './Icons/PageListIcon';
-import ShareLinkIcon from './Icons/ShareLinkIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-
-const PageAccessoriesModalControl = (props) => {
-  const { t } = useTranslation();
-  const {
-    pageAccessoriesContainer, isGuestUser, isSharedUser,
-  } = props;
-  const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
-
-  const { data: pageId } = useCurrentPageId();
-
-  const accessoriesBtnList = useMemo(() => {
-    return [
-      {
-        name: 'pagelist',
-        Icon: <PageListIcon />,
-        disabled: isSharedUser,
-        i18n: t('page_list'),
-      },
-      {
-        name: 'timeline',
-        Icon: <TimeLineIcon />,
-        disabled: isSharedUser,
-        i18n: t('Timeline View'),
-      },
-      {
-        name: 'pageHistory',
-        Icon: <HistoryIcon />,
-        disabled: isGuestUser || isSharedUser,
-        i18n: t('History'),
-      },
-      {
-        name: 'attachment',
-        Icon: <AttachmentIcon />,
-        i18n: t('attachment_data'),
-      },
-      {
-        name: 'shareLink',
-        Icon: <ShareLinkIcon />,
-        disabled: isGuestUser || isSharedUser || isLinkSharingDisabled,
-        i18n: t('share_links.share_link_management'),
-      },
-    ];
-  }, [t, isGuestUser, isSharedUser, isLinkSharingDisabled]);
-
-  return (
-    <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
-      {accessoriesBtnList.map((accessory) => {
-
-        let tooltipMessage;
-        if (accessory.disabled) {
-          tooltipMessage = t('Not available for guest');
-          if (accessory.name === 'shareLink' && isLinkSharingDisabled) {
-            tooltipMessage = t('Link sharing is disabled');
-          }
-        }
-        else {
-          tooltipMessage = accessory.i18n;
-        }
-
-        return (
-          <Fragment key={accessory.name}>
-            <div id={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`}>
-              <button
-                type="button"
-                className={`btn btn-link grw-btn-page-accessories ${accessory.disabled ? 'disabled' : ''}`}
-                onClick={() => pageAccessoriesContainer.openPageAccessoriesModal(accessory.name)}
-              >
-                {accessory.Icon}
-              </button>
-            </div>
-            <UncontrolledTooltip placement="top" target={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`} fade={false}>
-              {tooltipMessage}
-            </UncontrolledTooltip>
-          </Fragment>
-        );
-      })}
-    </div>
-  );
-};
-
-PageAccessoriesModalControl.propTypes = {
-  pageAccessoriesContainer: PropTypes.any,
-
-  isGuestUser: PropTypes.bool.isRequired,
-  isSharedUser: PropTypes.bool.isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PageAccessoriesModalControlWrapper = withUnstatedContainers(PageAccessoriesModalControl, []);
-
-export default PageAccessoriesModalControlWrapper;

+ 2 - 4
packages/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -3,15 +3,13 @@ import React from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useXss } from '~/stores/xss';
 
 
 
 
 export const PageGrantAlert = (): JSX.Element => {
 export const PageGrantAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: pageData } = useSWRxCurrentPage();
   const { data: pageData } = useSWRxCurrentPage();
-  const { data: xss } = useXss();
 
 
-  if (pageData == null || pageData.grant == null || pageData.grant === 1 || xss == null) {
+  if (pageData == null || pageData.grant == null || pageData.grant === 1) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -34,7 +32,7 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 5) {
       if (pageData.grant === 5) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-organization"></i><strong>{xss.process(pageData.grantedGroup.name)} only</strong>
+            <i className="icon-fw icon-organization"></i><strong>{pageData.grantedGroup.name} only</strong>
           </>
           </>
         );
         );
       }
       }

+ 1 - 13
packages/app/src/components/PageEditor.tsx

@@ -399,27 +399,15 @@ const PageEditor = (props: Props): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
-  // const config = props.appContainer.getConfig();
-  // const isUploadable = config.upload.image || config.upload.file;
   const isUploadable = isUploadableImage || isUploadableFile;
   const isUploadable = isUploadableImage || isUploadableFile;
 
 
 
 
-  // TODO: omit no-explicit-any -- 2022.06.02 Yuki Takei
-  // It is impossible to avoid the error
-  //  "Property '...' does not exist on type 'IntrinsicAttributes & RefAttributes<any>'"
-  //  because Editor is a class component and must be wrapped with React.forwardRef
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  const EditorAny = Editor as any;
-
-  // console.log('EditorAny', markdown);
-
   return (
   return (
     <div className="d-flex flex-wrap">
     <div className="d-flex flex-wrap">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
-        <EditorAny
+        <Editor
           ref={editorRef}
           ref={editorRef}
           value={markdown}
           value={markdown}
-          isMobile={isMobile}
           isUploadable={isUploadable}
           isUploadable={isUploadable}
           isUploadableFile={isUploadableFile}
           isUploadableFile={isUploadableFile}
           isTextlintEnabled={isTextlintEnabled}
           isTextlintEnabled={isTextlintEnabled}

+ 0 - 3
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -987,9 +987,6 @@ class CodeMirrorEditor extends AbstractEditor {
       gutters.push('CodeMirror-lint-markers');
       gutters.push('CodeMirror-lint-markers');
     }
     }
 
 
-    console.log(' this.state.value', this.state.value);
-    console.log(' this.props.value', this.props.value);
-
     return (
     return (
       <div className={`grw-codemirror-editor ${styles['grw-codemirror-editor']}`}>
       <div className={`grw-codemirror-editor ${styles['grw-codemirror-editor']}`}>
 
 

+ 1 - 1
packages/app/src/components/PageEditor/DrawioModal.jsx

@@ -136,7 +136,7 @@ class DrawioModal extends React.PureComponent {
         isOpen={this.state.show}
         isOpen={this.state.show}
         toggle={this.cancel}
         toggle={this.cancel}
         backdrop="static"
         backdrop="static"
-        className="drawio-modal"
+        className="drawio-modal grw-body-only-modal-expanded"
         size="xl"
         size="xl"
         keyboard={false}
         keyboard={false}
       >
       >

+ 0 - 406
packages/app/src/components/PageEditor/Editor.jsx

@@ -1,406 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import Dropzone from 'react-dropzone';
-import {
-  Modal, ModalHeader, ModalBody,
-} from 'reactstrap';
-
-import { useDefaultIndentSize } from '~/stores/context';
-import { useEditorSettings } from '~/stores/editor';
-
-import AbstractEditor from './AbstractEditor';
-import Cheatsheet from './Cheatsheet';
-import CodeMirrorEditor from './CodeMirrorEditor';
-import pasteHelper from './PasteHelper';
-import TextAreaEditor from './TextAreaEditor';
-
-
-class Editor extends AbstractEditor {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isComponentDidMount: false,
-      dropzoneActive: false,
-      isUploading: false,
-      isCheatsheetModalShown: false,
-    };
-
-    this.getEditorSubstance = this.getEditorSubstance.bind(this);
-
-    this.pasteFilesHandler = this.pasteFilesHandler.bind(this);
-
-    this.dragEnterHandler = this.dragEnterHandler.bind(this);
-    this.dragLeaveHandler = this.dragLeaveHandler.bind(this);
-    this.dropHandler = this.dropHandler.bind(this);
-
-    this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
-    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
-
-    this.getAcceptableType = this.getAcceptableType.bind(this);
-    this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
-    this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
-  }
-
-  componentDidMount() {
-    this.setState({ isComponentDidMount: true });
-  }
-
-  getEditorSubstance() {
-    return this.props.isMobile
-      ? this.taEditor
-      : this.cmEditor;
-  }
-
-  /**
-   * @inheritDoc
-   */
-  forceToFocus() {
-    this.getEditorSubstance().forceToFocus();
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setValue(newValue) {
-    this.getEditorSubstance().setValue(newValue);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setGfmMode(bool) {
-    this.getEditorSubstance().setGfmMode(bool);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setCaretLine(line) {
-    this.getEditorSubstance().setCaretLine(line);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setScrollTopByLine(line) {
-    this.getEditorSubstance().setScrollTopByLine(line);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  insertText(text) {
-    this.getEditorSubstance().insertText(text);
-  }
-
-  /**
-   * remove overlay and set isUploading to false
-   */
-  terminateUploadingState() {
-    this.setState({
-      dropzoneActive: false,
-      isUploading: false,
-    });
-  }
-
-  /**
-   * dispatch onUpload event
-   */
-  dispatchUpload(files) {
-    if (this.props.onUpload != null) {
-      this.props.onUpload(files);
-    }
-  }
-
-  /**
-   * get acceptable(uploadable) file type
-   */
-  getAcceptableType() {
-    let accept = 'null'; // reject all
-    if (this.props.isUploadable) {
-      if (!this.props.isUploadableFile) {
-        accept = 'image/*'; // image only
-      }
-      else {
-        accept = ''; // allow all
-      }
-    }
-
-    return accept;
-  }
-
-  pasteFilesHandler(event) {
-    const items = event.clipboardData.items || event.clipboardData.files || [];
-
-    // abort if length is not 1
-    if (items.length < 1) {
-      return;
-    }
-
-    for (let i = 0; i < items.length; i++) {
-      try {
-        const file = items[i].getAsFile();
-        // check file type (the same process as Dropzone)
-        if (file != null && pasteHelper.isAcceptableType(file, this.getAcceptableType())) {
-          this.dispatchUpload(file);
-          this.setState({ isUploading: true });
-        }
-      }
-      catch (e) {
-        this.logger.error(e);
-      }
-    }
-  }
-
-  dragEnterHandler(event) {
-    const dataTransfer = event.dataTransfer;
-
-    // do nothing if contents is not files
-    if (!dataTransfer.types.includes('Files')) {
-      return;
-    }
-
-    this.setState({ dropzoneActive: true });
-  }
-
-  dragLeaveHandler() {
-    this.setState({ dropzoneActive: false });
-  }
-
-  dropHandler(accepted, rejected) {
-    // rejected
-    if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
-      this.setState({ dropzoneActive: false });
-      return;
-    }
-
-    const file = accepted[0];
-    this.dispatchUpload(file);
-    this.setState({ isUploading: true });
-  }
-
-  showMarkdownHelp() {
-    this.setState({ isCheatsheetModalShown: true });
-  }
-
-  addAttachmentHandler() {
-    this.dropzone.open();
-  }
-
-  getDropzoneClassName(isDragAccept, isDragReject) {
-    let className = 'dropzone';
-    if (!this.props.isUploadable) {
-      className += ' dropzone-unuploadable';
-    }
-    else {
-      className += ' dropzone-uploadable';
-
-      if (this.props.isUploadableFile) {
-        className += ' dropzone-uploadablefile';
-      }
-    }
-
-    // uploading
-    if (this.state.isUploading) {
-      className += ' dropzone-uploading';
-    }
-
-    if (isDragAccept) {
-      className += ' dropzone-accepted';
-    }
-
-    if (isDragReject) {
-      className += ' dropzone-rejected';
-    }
-
-    return className;
-  }
-
-  renderDropzoneOverlay() {
-    return (
-      <div className="overlay overlay-dropzone-active">
-        {this.state.isUploading
-          && (
-            <span className="overlay-content">
-              <div className="speeding-wheel d-inline-block"></div>
-              <span className="sr-only">Uploading...</span>
-            </span>
-          )
-        }
-        {!this.state.isUploading && <span className="overlay-content"></span>}
-      </div>
-    );
-  }
-
-  renderNavbar() {
-    return (
-      <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
-        <ul className="pl-2 nav nav-navbar">
-          { this.getNavbarItems() != null && this.getNavbarItems().map((item, idx) => {
-            // eslint-disable-next-line react/no-array-index-key
-            return <li key={`navbarItem-${idx}`}>{item}</li>;
-          }) }
-        </ul>
-      </div>
-    );
-  }
-
-  getNavbarItems() {
-    // set navbar items(react elements) here that are common in CodeMirrorEditor or TextAreaEditor
-    const navbarItems = [];
-
-    // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
-    return navbarItems.concat(this.getEditorSubstance().getNavbarItems());
-  }
-
-  renderCheatsheetModal() {
-    const hideCheatsheetModal = () => {
-      this.setState({ isCheatsheetModalShown: false });
-    };
-
-    return (
-      <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
-        <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <i className="icon-fw icon-question" />Markdown help
-        </ModalHeader>
-        <ModalBody>
-          <Cheatsheet />
-        </ModalBody>
-      </Modal>
-    );
-  }
-
-
-  render() {
-    const flexContainer = {
-      height: '100%',
-      display: 'flex',
-      flexDirection: 'column',
-    };
-
-    const {
-      isMobile,
-      indentSize,
-    } = this.props;
-
-    return (
-      <>
-        <div style={flexContainer} className="editor-container">
-          <Dropzone
-            ref={(c) => { this.dropzone = c }}
-            accept={this.getAcceptableType()}
-            noClick
-            noKeyboard
-            multiple={false}
-            onDragLeave={this.dragLeaveHandler}
-            onDrop={this.dropHandler}
-          >
-            {({
-              getRootProps,
-              getInputProps,
-              isDragAccept,
-              isDragReject,
-            }) => {
-              return (
-                <div className={this.getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
-                  { this.state.dropzoneActive && this.renderDropzoneOverlay() }
-
-                  { this.state.isComponentDidMount && this.renderNavbar() }
-
-                  {/* for PC */}
-                  { !isMobile && (
-                    // eslint-disable-next-line arrow-body-style
-                    <CodeMirrorEditor
-                      ref={(c) => { this.cmEditor = c }}
-                      indentSize={indentSize}
-                      onPasteFiles={this.pasteFilesHandler}
-                      onDragEnter={this.dragEnterHandler}
-                      onMarkdownHelpButtonClicked={this.showMarkdownHelp}
-                      onAddAttachmentButtonClicked={this.addAttachmentHandler}
-                      {...this.props}
-                    />
-                  )}
-
-                  {/* for mobile */}
-                  { isMobile && (
-                    <TextAreaEditor
-                      ref={(c) => { this.taEditor = c }}
-                      onPasteFiles={this.pasteFilesHandler}
-                      onDragEnter={this.dragEnterHandler}
-                      {...this.props}
-                    />
-                  )}
-
-                  <input {...getInputProps()} />
-                </div>
-              );
-            }}
-          </Dropzone>
-
-          { this.props.isUploadable
-            && (
-              <button
-                type="button"
-                className="btn btn-outline-secondary btn-block btn-open-dropzone"
-                onClick={this.addAttachmentHandler}
-              >
-                <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
-                Attach files
-                <span className="d-none d-sm-inline">
-                &nbsp;by dragging &amp; dropping,&nbsp;
-                  <span className="btn-link">selecting them</span>,&nbsp;
-                  or pasting from the clipboard.
-                </span>
-
-              </button>
-            )
-          }
-
-          { this.renderCheatsheetModal() }
-
-        </div>
-      </>
-    );
-  }
-
-}
-
-Editor.propTypes = Object.assign({
-  noCdn: PropTypes.bool,
-  // this value is markdown
-  value: PropTypes.string,
-  isMobile: PropTypes.bool,
-  isUploadable: PropTypes.bool,
-  isUploadableFile: PropTypes.bool,
-  onChange: PropTypes.func,
-  onUpload: PropTypes.func,
-  editorSettings: PropTypes.object.isRequired,
-  indentSize: PropTypes.number,
-}, AbstractEditor.propTypes);
-
-
-const EditorWrapper = React.forwardRef((props, ref) => {
-  const { data: editorSettings } = useEditorSettings();
-  const { data: defaultIndentSize } = useDefaultIndentSize();
-
-  if (editorSettings == null) {
-    return <></>;
-  }
-
-  return (
-    <Editor
-      ref={ref}
-      {...props}
-      editorSettings={editorSettings}
-      // eslint-disable-next-line react/prop-types
-      indentSize={props.indentSize ?? defaultIndentSize}
-    />
-  );
-});
-
-EditorWrapper.displayName = 'EditorWrapper';
-
-export default EditorWrapper;

+ 365 - 0
packages/app/src/components/PageEditor/Editor.tsx

@@ -0,0 +1,365 @@
+import React, {
+  useState, useRef, useImperativeHandle, useCallback, useMemo,
+} from 'react';
+
+import Dropzone from 'react-dropzone';
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody,
+} from 'reactstrap';
+
+import { toastError } from '~/client/util/apiNotification';
+import { useDefaultIndentSize } from '~/stores/context';
+import { useEditorSettings } from '~/stores/editor';
+import { useIsMobile } from '~/stores/ui';
+
+import { IEditorMethods } from '../../interfaces/editor-methods';
+
+import Cheatsheet from './Cheatsheet';
+import CodeMirrorEditor from './CodeMirrorEditor';
+import pasteHelper from './PasteHelper';
+import TextAreaEditor from './TextAreaEditor';
+
+
+type EditorPropsType = {
+  value?: string,
+  isGfmMode?: boolean,
+  noCdn?: boolean,
+  isUploadable?: boolean,
+  isUploadableFile?: boolean,
+  isTextlintEnabled?: boolean,
+  onChange?: (newValue: string) => void,
+  onUpload?: (file) => void,
+  indentSize?: number,
+  onScroll?: ({ line: number }) => void,
+  onScrollCursorIntoView?: (line: number) => void,
+  onSave?: () => Promise<void>,
+  onPasteFiles?: (event: Event) => void,
+  onCtrlEnter?: (event: Event) => void,
+}
+
+type DropzoneRef = {
+  open: () => void
+}
+
+const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
+  const {
+    onUpload, isUploadable, isUploadableFile, indentSize, isGfmMode = true,
+  } = props;
+
+  const [dropzoneActive, setDropzoneActive] = useState(false);
+  const [isUploading, setIsUploading] = useState(false);
+  const [isCheatsheetModalShown, setIsCheatsheetModalShown] = useState(false);
+
+  const { t } = useTranslation();
+  const { data: editorSettings } = useEditorSettings();
+  const { data: defaultIndentSize } = useDefaultIndentSize();
+  const { data: isMobile } = useIsMobile();
+
+  const dropzoneRef = useRef<DropzoneRef>(null);
+  const cmEditorRef = useRef<CodeMirrorEditor>(null);
+  const taEditorRef = useRef<TextAreaEditor>(null);
+
+  const editorSubstance = isMobile ? taEditorRef.current : cmEditorRef.current;
+
+  const methods: Partial<IEditorMethods> = useMemo(() => {
+    return {
+      forceToFocus: () => {
+        if (editorSubstance == null) { return }
+        editorSubstance.forceToFocus();
+      },
+      setValue: (newValue: string) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setValue(newValue);
+      },
+      setGfmMode: (bool: boolean) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setGfmMode(bool);
+      },
+      setCaretLine: (line: number) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setCaretLine(line);
+      },
+      setScrollTopByLine: (line: number) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setScrollTopByLine(line);
+      },
+      insertText: (text: string) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.insertText(text);
+      },
+      getNavbarItems: (): JSX.Element[] => {
+        if (editorSubstance == null) { return [] }
+        // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
+        const navbarItems = editorSubstance.getNavbarItems() ?? [];
+        return navbarItems;
+      },
+    };
+  }, [editorSubstance]);
+
+  // methods for ref
+  useImperativeHandle(ref, () => ({
+    forceToFocus: methods.forceToFocus,
+    setValue: methods.setValue,
+    setGfmMode: methods.setGfmMode,
+    setCaretLine: methods.setCaretLine,
+    setScrollTopByLine: methods.setScrollTopByLine,
+    insertText: methods.insertText,
+    /**
+   * remove overlay and set isUploading to false
+   */
+    terminateUploadingState: () => {
+      setDropzoneActive(false);
+      setIsUploading(false);
+    },
+  }));
+
+  /**
+   * dispatch onUpload event
+   */
+  const dispatchUpload = useCallback((files) => {
+    if (onUpload != null) {
+      onUpload(files);
+    }
+  }, [onUpload]);
+
+  /**
+   * get acceptable(uploadable) file type
+   */
+  const getAcceptableType = useCallback(() => {
+    let accept = 'null'; // reject all
+    if (isUploadable) {
+      if (!isUploadableFile) {
+        accept = 'image/*'; // image only
+      }
+      else {
+        accept = ''; // allow all
+      }
+    }
+
+    return accept;
+  }, [isUploadable, isUploadableFile]);
+
+  const pasteFilesHandler = useCallback((event) => {
+    const items = event.clipboardData.items || event.clipboardData.files || [];
+
+    toastError(t('toaster.file_upload_failed'));
+
+    // abort if length is not 1
+    if (items.length < 1) {
+      return;
+    }
+
+    for (let i = 0; i < items.length; i++) {
+      try {
+        const file = items[i].getAsFile();
+        // check file type (the same process as Dropzone)
+        if (file != null && pasteHelper.isAcceptableType(file, getAcceptableType())) {
+          dispatchUpload(file);
+          setIsUploading(true);
+        }
+      }
+      catch (e) {
+        toastError(t('toaster.file_upload_failed'));
+      }
+    }
+  }, [dispatchUpload, getAcceptableType, t]);
+
+  const dragEnterHandler = useCallback((event) => {
+    const dataTransfer = event.dataTransfer;
+
+    // do nothing if contents is not files
+    if (!dataTransfer.types.includes('Files')) {
+      return;
+    }
+
+    setDropzoneActive(true);
+  }, []);
+
+  const dropHandler = useCallback((accepted) => {
+    // rejected
+    if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
+      setDropzoneActive(false);
+      return;
+    }
+
+    const file = accepted[0];
+    dispatchUpload(file);
+    setIsUploading(true);
+  }, [dispatchUpload]);
+
+  const addAttachmentHandler = useCallback(() => {
+    if (dropzoneRef.current == null) { return }
+    dropzoneRef.current.open();
+  }, []);
+
+  const getDropzoneClassName = useCallback((isDragAccept: boolean, isDragReject: boolean) => {
+    let className = 'dropzone';
+    if (!isUploadable) {
+      className += ' dropzone-unuploadable';
+    }
+    else {
+      className += ' dropzone-uploadable';
+
+      if (isUploadableFile) {
+        className += ' dropzone-uploadablefile';
+      }
+    }
+
+    // uploading
+    if (isUploading) {
+      className += ' dropzone-uploading';
+    }
+
+    if (isDragAccept) {
+      className += ' dropzone-accepted';
+    }
+
+    if (isDragReject) {
+      className += ' dropzone-rejected';
+    }
+
+    return className;
+  }, [isUploadable, isUploading, isUploadableFile]);
+
+  const renderDropzoneOverlay = useCallback(() => {
+    return (
+      <div className="overlay overlay-dropzone-active">
+        {isUploading
+          && (
+            <span className="overlay-content">
+              <div className="speeding-wheel d-inline-block"></div>
+              <span className="sr-only">Uploading...</span>
+            </span>
+          )
+        }
+        {!isUploading && <span className="overlay-content"></span>}
+      </div>
+    );
+  }, [isUploading]);
+
+  const renderNavbar = useCallback(() => {
+    return (
+      <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
+        <ul className="pl-2 nav nav-navbar">
+          { methods.getNavbarItems?.().map((item, idx) => {
+            // eslint-disable-next-line react/no-array-index-key
+            return <li key={`navbarItem-${idx}`}>{item}</li>;
+          }) }
+        </ul>
+      </div>
+    );
+  }, [methods]);
+
+  const renderCheatsheetModal = useCallback(() => {
+    const hideCheatsheetModal = () => {
+      setIsCheatsheetModalShown(false);
+    };
+
+    return (
+      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
+        <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
+          <i className="icon-fw icon-question" />Markdown help
+        </ModalHeader>
+        <ModalBody>
+          <Cheatsheet />
+        </ModalBody>
+      </Modal>
+    );
+  }, [isCheatsheetModalShown]);
+
+  if (editorSettings == null) {
+    return <></>;
+  }
+
+  const flexContainer: React.CSSProperties = {
+    height: '100%',
+    display: 'flex',
+    flexDirection: 'column',
+  };
+
+  return (
+    <>
+      <div style={flexContainer} className="editor-container">
+        <Dropzone
+          ref={dropzoneRef}
+          accept={getAcceptableType()}
+          noClick
+          noKeyboard
+          multiple={false}
+          onDragLeave={() => { setDropzoneActive(false) }}
+          onDrop={dropHandler}
+        >
+          {({
+            getRootProps,
+            getInputProps,
+            isDragAccept,
+            isDragReject,
+          }) => {
+            return (
+              <div className={getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
+                { dropzoneActive && renderDropzoneOverlay() }
+
+                { renderNavbar() }
+
+                {/* for PC */}
+                { !isMobile && (
+                  // eslint-disable-next-line arrow-body-style
+                  <CodeMirrorEditor
+                    ref={cmEditorRef}
+                    indentSize={indentSize ?? defaultIndentSize}
+                    onPasteFiles={pasteFilesHandler}
+                    onDragEnter={dragEnterHandler}
+                    onMarkdownHelpButtonClicked={() => { setIsCheatsheetModalShown(true) }}
+                    onAddAttachmentButtonClicked={addAttachmentHandler}
+                    editorSettings={editorSettings}
+                    isGfmMode={isGfmMode}
+                    {...props}
+                  />
+                )}
+
+                {/* for mobile */}
+                { isMobile && (
+                  <TextAreaEditor
+                    ref={taEditorRef}
+                    onPasteFiles={pasteFilesHandler}
+                    onDragEnter={dragEnterHandler}
+                    {...props}
+                  />
+                )}
+
+                <input {...getInputProps()} />
+              </div>
+            );
+          }}
+        </Dropzone>
+
+        { isUploadable
+          && (
+            <button
+              type="button"
+              className="btn btn-outline-secondary btn-block btn-open-dropzone"
+              onClick={addAttachmentHandler}
+            >
+              <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
+              Attach files
+              <span className="d-none d-sm-inline">
+              &nbsp;by dragging &amp; dropping,&nbsp;
+                <span className="btn-link">selecting them</span>,&nbsp;
+                or pasting from the clipboard.
+              </span>
+
+            </button>
+          )
+        }
+
+        { renderCheatsheetModal() }
+
+      </div>
+    </>
+  );
+});
+
+Editor.displayName = 'Editor';
+
+export default Editor;

+ 15 - 13
packages/app/src/components/PageList/PageListItemL.tsx

@@ -8,6 +8,7 @@ import { DevidedPagePath } from '@growi/core';
 import { UserPicture, PageListMeta } from '@growi/ui';
 import { UserPicture, PageListMeta } from '@growi/ui';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 import Clamp from 'react-multiline-clamp';
 import { CustomInput } from 'reactstrap';
 import { CustomInput } from 'reactstrap';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
@@ -99,7 +100,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
   const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
 
 
   useEffect(() => {
   useEffect(() => {
-    if (isIPageInfoForEntity(pageInfo) && pageInfo != null) {
+    if (isIPageInfoForEntity(pageInfo)) {
       // likerCount
       // likerCount
       setLikerCount(pageInfo.likerIds?.length ?? 0);
       setLikerCount(pageInfo.likerIds?.length ?? 0);
       // bookmarkCount
       // bookmarkCount
@@ -201,18 +202,19 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                 <span className="h5 mb-0">
                 <span className="h5 mb-0">
                   {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
                   {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
                   <span className="grw-page-path-hierarchical-link text-break">
                   <span className="grw-page-path-hierarchical-link text-break">
-                    {shouldDangerouslySetInnerHTMLForPaths
-                      ? (
-                        <a
-                          className="page-segment"
-                          href={encodeURI(urljoin('/', pageData._id))}
-                          // eslint-disable-next-line react/no-danger
-                          dangerouslySetInnerHTML={{ __html: linkedPagePathHighlightedLatter.pathName }}
-                        >
-                        </a>
-                      )
-                      : <a className="page-segment" href={encodeURI(urljoin('/', pageData._id))}>{linkedPagePathHighlightedLatter.pathName}</a>
-                    }
+                    <Link href={encodeURI(urljoin('/', pageData._id))} prefetch={false}>
+                      {shouldDangerouslySetInnerHTMLForPaths
+                        ? (
+                          <a
+                            className="page-segment"
+                            // eslint-disable-next-line react/no-danger
+                            dangerouslySetInnerHTML={{ __html: linkedPagePathHighlightedLatter.pathName }}
+                          >
+                          </a>
+                        )
+                        : <a className="page-segment">{linkedPagePathHighlightedLatter.pathName}</a>
+                      }
+                    </Link>
                   </span>
                   </span>
                 </span>
                 </span>
               </Clamp>
               </Clamp>

+ 3 - 3
packages/app/src/components/PagePathHierarchicalLink.tsx

@@ -31,7 +31,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
       ? (
       ? (
         <>
         <>
           <span className="path-segment">
           <span className="path-segment">
-            <Link href="/trash">
+            <Link href="/trash" prefetch={false}>
               <a ><i className="icon-trash"></i></a>
               <a ><i className="icon-trash"></i></a>
             </Link>
             </Link>
           </span>
           </span>
@@ -41,7 +41,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
       : (
       : (
         <>
         <>
           <span className="path-segment">
           <span className="path-segment">
-            <Link href="/">
+            <Link href="/" prefetch={false}>
               <a >
               <a >
                 <i className="icon-home"></i>
                 <i className="icon-home"></i>
                 <span className="separator">/</span>
                 <span className="separator">/</span>
@@ -82,7 +82,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
         <span className="separator">/</span>
         <span className="separator">/</span>
       ) }
       ) }
 
 
-      <Link href={href}>
+      <Link href={href} prefetch={false}>
         {
         {
           shouldDangerouslySetInnerHTML
           shouldDangerouslySetInnerHTML
             // eslint-disable-next-line react/no-danger
             // eslint-disable-next-line react/no-danger

+ 1 - 1
packages/app/src/components/PagePresentationModal.jsx

@@ -17,7 +17,7 @@ const PagePresentationModal = () => {
       isOpen={presentationData.isOpened}
       isOpen={presentationData.isOpened}
       toggle={closePresentationModal}
       toggle={closePresentationModal}
       data-testid="page-presentation-modal"
       data-testid="page-presentation-modal"
-      className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
+      className={`grw-presentation-modal ${styles['grw-presentation-modal']} grw-body-only-modal-expanded`}
       unmountOnClose={false}
       unmountOnClose={false}
     >
     >
       <ModalBody className="modal-body">
       <ModalBody className="modal-body">

+ 0 - 4
packages/app/src/components/PagePresentationModal.module.scss

@@ -1,8 +1,4 @@
-@use '~/styles/mixins' as mi;
-
 .grw-presentation-modal :global {
 .grw-presentation-modal :global {
-  @include mi.expand-modal-fullscreen(false, false);
-
   .modal-body {
   .modal-body {
     background: black;
     background: black;
 
 

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

@@ -0,0 +1,14 @@
+@use '../styles/bootstrap/variables' as var;
+
+.card-timeline {
+  :global {
+    border: 1px solid var.$gray-300;
+  }
+
+  &:global {
+    > .card-header {
+      background-color: var.$gray-300;
+    }
+  }
+}
+

+ 10 - 3
packages/app/src/components/PageTimeline.tsx

@@ -1,6 +1,7 @@
-import React, { useEffect, useState, useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
@@ -10,6 +11,8 @@ import { useTimelineOptions } from '~/stores/renderer';
 import RevisionLoader from './Page/RevisionLoader';
 import RevisionLoader from './Page/RevisionLoader';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 
 
+import styles from './PageTimeline.module.scss';
+
 export const PageTimeline = (): JSX.Element => {
 export const PageTimeline = (): JSX.Element => {
   const [activePage, setActivePage] = useState(1);
   const [activePage, setActivePage] = useState(1);
   const [totalPageItems, setTotalPageItems] = useState(0);
   const [totalPageItems, setTotalPageItems] = useState(0);
@@ -51,8 +54,12 @@ export const PageTimeline = (): JSX.Element => {
       { pages.map((page) => {
       { pages.map((page) => {
         return (
         return (
           <div className="timeline-body" key={`key-${page._id}`}>
           <div className="timeline-body" key={`key-${page._id}`}>
-            <div className="card card-timeline">
-              <div className="card-header"><a href={page.path}>{page.path}</a></div>
+            <div className={`card card-timeline ${styles['card-timeline']}`}>
+              <div className="card-header">
+                <Link href={page.path} prefetch={false}>
+                  <a>{page.path}</a>
+                </Link>
+              </div>
               <div className="card-body">
               <div className="card-body">
                 <RevisionLoader
                 <RevisionLoader
                   lazy
                   lazy

+ 1 - 12
packages/app/src/components/PrivateLegacyPages.tsx

@@ -8,7 +8,6 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
@@ -190,21 +189,12 @@ ConvertByPathModal.displayName = 'ConvertByPathModal';
  * LegacyPage
  * LegacyPage
  */
  */
 
 
-type Props = {
-  appContainer: AppContainer,
-}
-
-const PrivateLegacyPages = (props: Props): JSX.Element => {
+const PrivateLegacyPages = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
   const isAdmin = currentUser?.admin;
   const isAdmin = currentUser?.admin;
 
 
-  const {
-    appContainer,
-  } = props;
-
-
   const [keyword, setKeyword] = useState<string>(initQ);
   const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);
   const [limit, setLimit] = useState<number>(INITIAL_PAGING_SIZE);
   const [limit, setLimit] = useState<number>(INITIAL_PAGING_SIZE);
@@ -442,7 +432,6 @@ const PrivateLegacyPages = (props: Props): JSX.Element => {
     <>
     <>
       <SearchPageBase
       <SearchPageBase
         ref={searchPageBaseRef}
         ref={searchPageBaseRef}
-        appContainer={appContainer}
         pages={data?.data}
         pages={data?.data}
         onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
         onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
         forceHideMenuItems={[MenuItemType.BOOKMARK, MenuItemType.RENAME, MenuItemType.DUPLICATE, MenuItemType.REVERT, MenuItemType.PATH_RECOVERY]}
         forceHideMenuItems={[MenuItemType.BOOKMARK, MenuItemType.RENAME, MenuItemType.DUPLICATE, MenuItemType.REVERT, MenuItemType.PATH_RECOVERY]}

+ 22 - 0
packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss

@@ -0,0 +1,22 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+
+.code-inline {
+  padding: 2px 4px;
+  font-family: var.$font-family-monospace-not-strictly;
+  border: 1px solid;
+  border-radius: bs.$border-radius;
+}
+
+.code-highlighted-title {
+  position: absolute;
+  top: 0;
+  right: 0.5em;
+  padding: 0 4px;
+  font-style: normal;
+  font-weight: bold;
+  color: bs.$gray-900;
+  background: bs.$gray-300;
+  border-radius: bs.$border-radius;
+  opacity: 0.6;
+}

+ 33 - 0
packages/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -0,0 +1,33 @@
+import { CodeComponent } from 'react-markdown/lib/ast-to-react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
+
+import styles from './CodeBlock.module.scss';
+
+export const CodeBlock: CodeComponent = ({ inline, className, children }) => {
+  if (inline) {
+    return <code className={`code-inline ${styles['code-inline']} ${className ?? ''}`}>{children}</code>;
+  }
+
+  // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
+
+  const match = /language-(\w+)(:?.+)?/.exec(className || '');
+  const lang = match && match[1] ? match[1] : '';
+  const name = match && match[2] ? match[2].slice(1) : null;
+
+  return (
+    <>
+      {name != null && (
+        <cite className={`code-highlighted-title ${styles['code-highlighted-title']}`}>{name}</cite>
+      )}
+      <SyntaxHighlighter
+        className="code-highlighted"
+        PreTag="div"
+        style={oneLight}
+        language={lang}
+      >
+        {String(children).replace(/\n$/, '')}
+      </SyntaxHighlighter>
+    </>
+  );
+};

+ 2 - 2
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -39,8 +39,8 @@ export const NextLink = ({
   }
   }
 
 
   return (
   return (
-    <Link {...props} href={href}>
-      <a className={className}>{children}</a>
+    <Link {...props} href={href} prefetch={false}>
+      <a href={href} className={className}>{children}</a>
     </Link>
     </Link>
   );
   );
 };
 };

+ 5 - 23
packages/app/src/components/SearchPage.tsx

@@ -2,13 +2,12 @@ import React, {
   useCallback, useEffect, useMemo, useRef, useState,
   useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 
 
-import { parse as parseQuerystring } from 'querystring';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 
 
 
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import AppContainer from '~/client/services/AppContainer';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { useIsSearchServiceReachable } from '~/stores/context';
 import { useIsSearchServiceReachable } from '~/stores/context';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
@@ -88,29 +87,13 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
 SearchResultListHead.displayName = 'SearchResultListHead';
 SearchResultListHead.displayName = 'SearchResultListHead';
 
 
 
 
-/**
- * SearchPage
- */
-
-const getParsedUrlQuery = () => {
-  const search = window.location.search || '?';
-  return parseQuerystring(search.slice(1)); // remove heading '?' and parse
-};
-
-type Props = {
-  appContainer: AppContainer,
-}
-
-export const SearchPage = (props: Props): JSX.Element => {
+export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-
-  const {
-    appContainer,
-  } = props;
+  const router = useRouter();
 
 
   // parse URL Query
   // parse URL Query
-  const parsedQueries = getParsedUrlQuery().q;
-  const initQ = (Array.isArray(parsedQueries) ? parsedQueries.join(' ') : parsedQueries) ?? '';
+  const queries = router.query.q;
+  const initQ = (Array.isArray(queries) ? queries.join(' ') : queries) ?? '';
 
 
   const [keyword, setKeyword] = useState<string>(initQ);
   const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);
@@ -272,7 +255,6 @@ export const SearchPage = (props: Props): JSX.Element => {
   return (
   return (
     <SearchPageBase
     <SearchPageBase
       ref={searchPageBaseRef}
       ref={searchPageBaseRef}
-      appContainer={appContainer}
       pages={data?.data}
       pages={data?.data}
       searchingKeyword={keyword}
       searchingKeyword={keyword}
       onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
       onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}

+ 10 - 8
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -3,6 +3,7 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { exportAsMarkdown } from '~/client/services/page-operation';
@@ -19,13 +20,9 @@ import { useSearchResultOptions } from '~/stores/renderer';
 import { useFullTextSearchTermManager } from '~/stores/search';
 import { useFullTextSearchTermManager } from '~/stores/search';
 
 
 
 
-import AppContainer from '../../client/services/AppContainer';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { GrowiSubNavigation } from '../Navbar/GrowiSubNavigation';
-import { SubNavButtons } from '../Navbar/SubNavButtons';
-import RevisionLoader from '../Page/RevisionLoader';
-import { PageComment } from '../PageComment';
-import PageContentFooter from '../PageContentFooter';
+import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
+import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
 
 
 
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
@@ -54,7 +51,6 @@ const SCROLL_OFFSET_TOP = 175; // approximate height of (navigation + subnavigat
 const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true };
 const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true };
 
 
 type Props ={
 type Props ={
-  appContainer: AppContainer,
   pageWithMeta : IPageWithSearchMeta,
   pageWithMeta : IPageWithSearchMeta,
   highlightKeywords?: string[],
   highlightKeywords?: string[],
   showPageControlDropdown?: boolean,
   showPageControlDropdown?: boolean,
@@ -81,6 +77,12 @@ const generateObserverCallback = (doScroll: ()=>void) => {
 };
 };
 
 
 export const SearchResultContent: FC<Props> = (props: Props) => {
 export const SearchResultContent: FC<Props> = (props: Props) => {
+  const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
+  const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
+  const RevisionLoader = dynamic(() => import('../Page/RevisionLoader'), { ssr: false });
+  const PageComment = dynamic(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
+  const PageContentFooter = dynamic(() => import('../PageContentFooter'), { ssr: false });
+
   const scrollElementRef = useRef(null);
   const scrollElementRef = useRef(null);
 
 
   // for mutation
   // for mutation
@@ -106,7 +108,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
   const {
   const {
-    appContainer,
     pageWithMeta,
     pageWithMeta,
     highlightKeywords,
     highlightKeywords,
     showPageControlDropdown,
     showPageControlDropdown,
@@ -175,6 +176,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       ? page.revision
       ? page.revision
       : page.revision._id;
       : page.revision._id;
 
 
+
     return (
     return (
       <div className="d-flex flex-column align-items-end justify-content-center py-md-2">
       <div className="d-flex flex-column align-items-end justify-content-center py-md-2">
         <SubNavButtons
         <SubNavButtons

+ 0 - 1
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -125,7 +125,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     advanceFts();
     advanceFts();
   };
   };
 
 
-
   return (
   return (
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
       { (injectedPages ?? pages).map((page, i) => {
       { (injectedPages ?? pages).map((page, i) => {

+ 2 - 6
packages/app/src/components/SearchPage2/SearchPageBase.tsx

@@ -3,9 +3,9 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 
 
 import { ISelectableAll } from '~/client/interfaces/selectable-all';
 import { ISelectableAll } from '~/client/interfaces/selectable-all';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess } from '~/client/util/apiNotification';
 import { toastSuccess } from '~/client/util/apiNotification';
 import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search';
 import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction } from '~/interfaces/ui';
@@ -14,7 +14,6 @@ import { usePageDeleteModal } from '~/stores/modal';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { SearchResultContent } from '../SearchPage/SearchResultContent';
 import { SearchResultList } from '../SearchPage/SearchResultList';
 import { SearchResultList } from '../SearchPage/SearchResultList';
 
 
 
 
@@ -28,8 +27,6 @@ export interface IReturnSelectedPageIds {
 
 
 
 
 type Props = {
 type Props = {
-  appContainer: AppContainer,
-
   pages?: IPageWithSearchMeta[],
   pages?: IPageWithSearchMeta[],
   searchingKeyword?: string,
   searchingKeyword?: string,
 
 
@@ -43,8 +40,8 @@ type Props = {
 }
 }
 
 
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
+  const SearchResultContent = dynamic(import('../SearchPage/SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
   const {
   const {
-    appContainer,
     pages,
     pages,
     searchingKeyword,
     searchingKeyword,
     forceHideMenuItems,
     forceHideMenuItems,
@@ -203,7 +200,6 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
         <div className="mw-0 flex-grow-1 flex-basis-0 d-none d-lg-block search-result-content">
         <div className="mw-0 flex-grow-1 flex-basis-0 d-none d-lg-block search-result-content">
           { selectedPageWithMeta != null && (
           { selectedPageWithMeta != null && (
             <SearchResultContent
             <SearchResultContent
-              appContainer={appContainer}
               pageWithMeta={selectedPageWithMeta}
               pageWithMeta={selectedPageWithMeta}
               highlightKeywords={highlightKeywords}
               highlightKeywords={highlightKeywords}
               showPageControlDropdown={!isGuestUser}
               showPageControlDropdown={!isGuestUser}

+ 50 - 29
packages/app/src/components/ShareLink/ShareLink.jsx → packages/app/src/components/ShareLink/ShareLink.tsx

@@ -1,71 +1,97 @@
-import React, { useState, useCallback } from 'react';
+import React, {
+  useState, useCallback, useEffect,
+} from 'react';
+// import useSWR from 'swr';
 
 
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import useSWR from 'swr';
+import { useTranslation } from 'react-i18next';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
+import { IResShareLinkList } from '~/interfaces/share-link';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import ShareLinkForm from './ShareLinkForm';
 import ShareLinkForm from './ShareLinkForm';
 import ShareLinkList from './ShareLinkList';
 import ShareLinkList from './ShareLinkList';
 
 
-const ShareLink = (props) => {
+type Props = {
+  pageContainer: PageContainer;
+}
+
+const ShareLink = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { pageId } = props.pageContainer.state;
-  const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState(false);
 
 
-  const fetchShareLinks = useCallback(async(endpoint, pageId) => {
-    const res = await apiv3Get(endpoint, { relatedPage: pageId });
-    return {
-      shareLinkList: res.data.shareLinksResult,
-    };
-  }, []);
+  // const fetchShareLinks = useCallback(async(endpoint, pageId) => {
+  //   const res = await apiv3Get(endpoint, { relatedPage: pageId });
+  //   return {
+  //     shareLinkList: res.data.shareLinksResult,
+  //   };
+  // }, []);
+
+  // const { data, isValidating, mutate } = useSWR('/share-links/', (endpoint => fetchShareLinks(endpoint, pageId)));
 
 
-  const { data, isValidating, mutate } = useSWR('/share-links/', (endpoint => fetchShareLinks(endpoint, pageId)));
+  // const toggleShareLinkFormHandler = useCallback(() => {
+  //   setIsOpenShareLinkForm(prev => !prev);
+  //   mutate();
+  // }, [mutate]);
+
+  // TODO: ureplace useCurrentPageId and remove pageContainer https://redmine.weseek.co.jp/issues/101565
+  const { pageContainer } = props;
+  const { pageId } = pageContainer.state;
+  const [shareLinks, setShareLinks] = useState<IResShareLinkList['shareLinksResult']>([]);
+  const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState<boolean>(false);
+
+  const retrieveShareLinks = useCallback(async() => {
+    try {
+      const res = await apiv3Get<IResShareLinkList>('/share-links/', { relatedPage: pageId });
+      const { shareLinksResult } = res.data;
+      setShareLinks(shareLinksResult);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [pageId]);
 
 
   const toggleShareLinkFormHandler = useCallback(() => {
   const toggleShareLinkFormHandler = useCallback(() => {
     setIsOpenShareLinkForm(prev => !prev);
     setIsOpenShareLinkForm(prev => !prev);
-    mutate();
-  }, [mutate]);
+    retrieveShareLinks();
+  }, [retrieveShareLinks]);
 
 
-  const deleteAllLinksButtonHandler = useCallback(async(pageId) => {
+  const deleteAllLinksButtonHandler = useCallback(async() => {
     try {
     try {
       const res = await apiv3Delete('/share-links/', { relatedPage: pageId });
       const res = await apiv3Delete('/share-links/', { relatedPage: pageId });
       const count = res.data.n;
       const count = res.data.n;
       toastSuccess(t('toaster.remove_share_link', { count }));
       toastSuccess(t('toaster.remove_share_link', { count }));
-      mutate();
+      // mutate();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [mutate, t]);
+  }, [t]);
 
 
   const deleteLinkById = useCallback(async(shareLinkId) => {
   const deleteLinkById = useCallback(async(shareLinkId) => {
     try {
     try {
       const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const { deletedShareLink } = res.data;
       const { deletedShareLink } = res.data;
       toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
       toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
-      mutate();
+      // mutate();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [mutate, t]);
+  }, [t]);
 
 
   return (
   return (
     <div className="container p-0" data-testid="share-link-management">
     <div className="container p-0" data-testid="share-link-management">
       <h3 className="grw-modal-head d-flex pb-2">
       <h3 className="grw-modal-head d-flex pb-2">
         { t('share_links.share_link_list') }
         { t('share_links.share_link_list') }
-        <button className="btn btn-danger ml-auto " type="button" onClick={() => deleteAllLinksButtonHandler(pageId)}>{t('delete_all')}</button>
+        <button className="btn btn-danger ml-auto " type="button" onClick={deleteAllLinksButtonHandler}>{t('delete_all')}</button>
       </h3>
       </h3>
-
       <div>
       <div>
         <ShareLinkList
         <ShareLinkList
-          shareLinks={!isValidating ? data.shareLinkList : []}
+        // !isValidating ? data.shareLinkList :
+          shareLinks={[]}
           onClickDeleteButton={deleteLinkById}
           onClickDeleteButton={deleteLinkById}
         />
         />
         <button
         <button
@@ -79,11 +105,6 @@ const ShareLink = (props) => {
       </div>
       </div>
     </div>
     </div>
   );
   );
-
-};
-
-ShareLink.propTypes = {
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 };
 
 
 const ShareLinkWrapper = withUnstatedContainers(ShareLink, [PageContainer]);
 const ShareLinkWrapper = withUnstatedContainers(ShareLink, [PageContainer]);

+ 3 - 3
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -21,7 +21,7 @@ import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
 
 
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import CountBadge from '../../Common/CountBadge';
 import CountBadge from '../../Common/CountBadge';
@@ -411,7 +411,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
 
   // Rename process
   // Rename process
   // Icon that draw attention from users for some actions
   // Icon that draw attention from users for some actions
-  const shouldShowAttentionIcon = !!page.processData?.Rename?.isProcessable;
+  const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
 
 
   return (
   return (
     <div
     <div
@@ -459,7 +459,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
                 </>
                 </>
               )}
               )}
 
 
-              <Link href={`/${page._id}`}>
+              <Link href={`/${page._id}`} prefetch={false}>
                 <a className="grw-pagetree-title-anchor flex-grow-1">
                 <a className="grw-pagetree-title-anchor flex-grow-1">
                   <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
                   <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
                 </a>
                 </a>

+ 3 - 1
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -17,8 +17,10 @@ import FormattedDistanceDate from '../FormattedDistanceDate';
 
 
 import InfiniteScroll from './InfiniteScroll';
 import InfiniteScroll from './InfiniteScroll';
 
 
+import TagLabelsStyles from '../Page/TagLabels.module.scss';
 import styles from './RecentChanges.module.scss';
 import styles from './RecentChanges.module.scss';
 
 
+
 const logger = loggerFactory('growi:History');
 const logger = loggerFactory('growi:History');
 
 
 type PageItemProps = {
 type PageItemProps = {
@@ -64,7 +66,7 @@ const LargePageItem = memo(({ page }: PageItemProps): JSX.Element => {
       return <></>;
       return <></>;
     }
     }
     return (
     return (
-      <Link key={tag.name} href={`/_search?q=tag:${tag.name}`}>
+      <Link key={tag.name} href={`/_search?q=tag:${tag.name}`} prefetch={false}>
         <a className="grw-tag-label badge badge-secondary mr-2 small">
         <a className="grw-tag-label badge badge-secondary mr-2 small">
           {tag.name}
           {tag.name}
         </a>
         </a>

+ 1 - 1
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -62,7 +62,7 @@ const SecondaryItem: FC<SecondaryItemProps> = memo((props: SecondaryItemProps) =
   const { iconName, href, isBlank } = props;
   const { iconName, href, isBlank } = props;
 
 
   return (
   return (
-    <Link href={href}>
+    <Link href={href} prefetch={false}>
       <a className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
       <a className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
         <i className="material-icons">{iconName}</i>
         <i className="material-icons">{iconName}</i>
       </a>
       </a>

+ 2 - 9
packages/app/src/components/Skelton.tsx

@@ -1,24 +1,17 @@
 import React from 'react';
 import React from 'react';
 
 
 type SkeltonProps = {
 type SkeltonProps = {
-  width?: number,
-  height?: number,
   additionalClass?: string,
   additionalClass?: string,
   roundedPill?: boolean,
   roundedPill?: boolean,
 }
 }
 
 
 export const Skelton = (props: SkeltonProps): JSX.Element => {
 export const Skelton = (props: SkeltonProps): JSX.Element => {
   const {
   const {
-    width, height, additionalClass, roundedPill,
+    additionalClass, roundedPill,
   } = props;
   } = props;
 
 
-  const skeltonStyle = {
-    width,
-    height,
-  };
-
   return (
   return (
-    <div style={skeltonStyle} className={`${additionalClass}`}>
+    <div className={`${additionalClass}`}>
       <div className={`grw-skelton h-100 w-100 ${roundedPill ? 'rounded-pill' : ''}`}></div>
       <div className={`grw-skelton h-100 w-100 ${roundedPill ? 'rounded-pill' : ''}`}></div>
     </div>
     </div>
   );
   );

+ 11 - 0
packages/app/src/components/TableOfContents.module.scss

@@ -1,3 +1,4 @@
+@use '~/styles/bootstrap/init' as bs;
 
 
 .revision-toc :global {
 .revision-toc :global {
   // to get on the Attachment row
   // to get on the Attachment row
@@ -26,3 +27,13 @@
     }
     }
   }
   }
 }
 }
+
+.revision-toc {
+  @media print {
+    float: none;
+    max-width: 100%;
+    margin-bottom: 20px;
+    font-size: 0.9em;
+    border: solid 1px bs.$gray-400;
+  }
+}

+ 19 - 13
packages/app/src/components/TableOfContents.tsx

@@ -11,6 +11,10 @@ import loggerFactory from '~/utils/logger';
 
 
 import { StickyStretchableScroller } from './StickyStretchableScroller';
 import { StickyStretchableScroller } from './StickyStretchableScroller';
 
 
+
+import styles from './TableOfContents.module.scss';
+
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 const logger = loggerFactory('growi:TableOfContents');
 
 
@@ -53,20 +57,22 @@ const TableOfContents = (): JSX.Element => {
   }, [tocHtml]);
   }, [tocHtml]);
 
 
   return (
   return (
-    <StickyStretchableScroller
-      stickyElemSelector=".grw-side-contents-sticky-container"
-      calcViewHeight={calcViewHeight}
-    >
-      <div
-        id="revision-toc-content"
-        className="revision-toc-content mb-3"
+    <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>
+      <StickyStretchableScroller
+        stickyElemSelector=".grw-side-contents-sticky-container"
+        calcViewHeight={calcViewHeight}
       >
       >
-        {/* parse blank to show toc (https://github.com/weseek/growi/pull/6277) */}
-        <ReactMarkdown {...rendererOptions}>
-          {''}
-        </ReactMarkdown>
-      </div>
-    </StickyStretchableScroller>
+        <div
+          id="revision-toc-content"
+          className="revision-toc-content mb-3"
+        >
+          {/* parse blank to show toc (https://github.com/weseek/growi/pull/6277) */}
+          <ReactMarkdown {...rendererOptions}>
+            {''}
+          </ReactMarkdown>
+        </div>
+      </StickyStretchableScroller>
+    </div>
   );
   );
 
 
 };
 };

+ 7 - 7
packages/app/src/components/Theme/ThemeFuture.module.scss

@@ -1,12 +1,12 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 
 $primary: #00b5b7;
 $primary: #00b5b7;
 $themecolor: #16282d;
 $themecolor: #16282d;
 $accentcolor: #00fff5;
 $accentcolor: #00fff5;
 
 
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   // Background colors
   $bgcolor-global: $themecolor;
   $bgcolor-global: $themecolor;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-inline-code: #1f1f22; //optional
@@ -83,13 +83,13 @@ html[dark] {
   // admin theme box
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
   $color-theme-color-box: lighten($primary, 20%);
 
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
 
   //Button
   //Button
   .btn-group.grw-page-editor-mode-manager {
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(lighten($primary, 10%), $primary, darken($primary, 10%), darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 10%), $primary, darken($primary, 10%), darken($primary, 20%));
     }
     }
   }
   }
 
 

+ 8 - 0
packages/app/src/components/Theme/ThemeFuture.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeFuture.module.scss';
+
+const ThemeFuture = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeFuture;

+ 7 - 7
packages/app/src/components/Theme/ThemeHalloween.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 
 $themecolor: #aa4a04;
 $themecolor: #aa4a04;
 $themelight: #f0f8ff;
 $themelight: #f0f8ff;
@@ -33,8 +34,7 @@ $light: lighten($secondary, 10%);
 
 
 //== Light Mode
 //== Light Mode
 //
 //
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   // Background colors
   $bgcolor-global: #050000;
   $bgcolor-global: #050000;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-inline-code: #1f1f22; //optional
@@ -105,13 +105,13 @@ html[dark] {
   // admin theme box
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
   $color-theme-color-box: lighten($primary, 20%);
 
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
 
   //Button
   //Button
   .btn-group.grw-page-editor-mode-manager {
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(lighten($primary, 35%), $primary, lighten($primary, 5%), darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 35%), $primary, lighten($primary, 5%), darken($primary, 20%));
     }
     }
   }
   }
 
 

+ 8 - 0
packages/app/src/components/Theme/ThemeHalloween.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeHalloween.module.scss';
+
+const ThemeHalloween = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeHalloween;

+ 13 - 12
packages/app/src/components/Theme/ThemeHufflepuff.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 
 // == Define Bootstrap theme colors
 // == Define Bootstrap theme colors
 //
 //
@@ -19,7 +20,7 @@
 
 
 //== Light Mode
 //== Light Mode
 //
 //
-html[light] {
+.theme[data-color-scheme='light'] :global {
   // Theme colors
   // Theme colors
   $themecolor: #eaab20;
   $themecolor: #eaab20;
   $themelight: #efe2cf;
   $themelight: #efe2cf;
@@ -91,16 +92,16 @@ html[light] {
   // admin theme box
   // admin theme box
   $color-theme-color-box: darken($primary, 5%);
   $color-theme-color-box: darken($primary, 5%);
 
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
 
   //Button
   //Button
   .btn.btn-outline-primary {
   .btn.btn-outline-primary {
-    @include btn-page-editor-mode-manager(darken($primary, 50%), darken($primary, 50%), lighten($primary, 20%));
+    @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 50%), darken($primary, 50%), lighten($primary, 20%));
   }
   }
   .btn-group.grw-page-editor-mode-manager {
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 70%), lighten($primary, 5%), lighten($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 70%), lighten($primary, 5%), lighten($primary, 20%));
     }
     }
   }
   }
 
 
@@ -156,7 +157,7 @@ html[light] {
   }
   }
 }
 }
 
 
-html[dark] {
+.theme[data-color-scheme='dark'] :global {
   // Theme colors
   // Theme colors
   $themecolor: #eaab20;
   $themecolor: #eaab20;
   $themedark: #3d3f38;
   $themedark: #3d3f38;
@@ -234,8 +235,8 @@ html[dark] {
   // admin theme box
   // admin theme box
   $color-theme-color-box: $primary;
   $color-theme-color-box: $primary;
 
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
 
   // Navs
   // Navs
   .nav-tabs {
   .nav-tabs {
@@ -260,11 +261,11 @@ html[dark] {
 
 
   // Button
   // Button
   .btn.btn-outline-primary {
   .btn.btn-outline-primary {
-    @include btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 10%), darken($primary, 30%));
+    @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 10%), darken($primary, 30%));
   }
   }
   .btn-group.grw-page-editor-mode-manager {
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 0%), darken($primary, 30%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 0%), darken($primary, 30%));
     }
     }
   }
   }
 
 

+ 8 - 0
packages/app/src/components/Theme/ThemeHufflepuff.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeHufflepuff.module.scss';
+
+const ThemeHufflepuff = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeHufflepuff;

+ 7 - 7
packages/app/src/components/Theme/ThemeKibela.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 
 $bgcolor-theme: rgb(18, 86, 163);
 $bgcolor-theme: rgb(18, 86, 163);
 $themelight: #f4f5f6;
 $themelight: #f4f5f6;
@@ -27,8 +28,7 @@ $lightthemecolor: rgba(181, 203, 247, 0.61);
 }
 }
 
 
 // Light Mode
 // Light Mode
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   // Background colors
   $bgcolor-navbar: white;
   $bgcolor-navbar: white;
   $bgcolor-navbar-active: $bgcolor-theme;
   $bgcolor-navbar-active: $bgcolor-theme;
@@ -98,13 +98,13 @@ html[dark] {
   // Sidebar list group
   // Sidebar list group
   $bgcolor-sidebar-list-group: #fafbff; // optional
   $bgcolor-sidebar-list-group: #fafbff; // optional
 
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
 
   //Button
   //Button
   .grw-page-editor-mode-manager {
   .grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 15%), lighten($primary, 45%), lighten($primary, 50%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 15%), lighten($primary, 45%), lighten($primary, 50%));
     }
     }
   }
   }
 }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeKibela.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeKibela.module.scss';
+
+const ThemeKibela = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeKibela;

+ 21 - 9
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -11,12 +11,16 @@ const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
 const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
 const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
 const ThemeDefault = dynamic(() => import('../ThemeDefault'));
 const ThemeDefault = dynamic(() => import('../ThemeDefault'));
 const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
 const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
-const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
+const ThemeFuture = dynamic(() => import('../ThemeFuture'));
+const ThemeHalloween = dynamic(() => import('../ThemeHalloween'));
+const ThemeHufflepuff = dynamic(() => import('../ThemeHufflepuff'));
 const ThemeIsland = dynamic(() => import('../ThemeIsland'));
 const ThemeIsland = dynamic(() => import('../ThemeIsland'));
-const ThemeSpring = dynamic(() => import('../ThemeSpring'));
+const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
+const ThemeKibela = dynamic(() => import('../ThemeKibela'));
+const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
 const ThemeNature = dynamic(() => import('../ThemeNature'));
 const ThemeNature = dynamic(() => import('../ThemeNature'));
+const ThemeSpring = dynamic(() => import('../ThemeSpring'));
 const ThemeWood = dynamic(() => import('../ThemeWood'));
 const ThemeWood = dynamic(() => import('../ThemeWood'));
-const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
 
 
 
 
 type Props = {
 type Props = {
@@ -34,18 +38,26 @@ export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
       return <ThemeChristmas>{children}</ThemeChristmas>;
       return <ThemeChristmas>{children}</ThemeChristmas>;
     case GrowiThemes.FIRE_RED:
     case GrowiThemes.FIRE_RED:
       return <ThemeFireRed>{children}</ThemeFireRed>;
       return <ThemeFireRed>{children}</ThemeFireRed>;
-    case GrowiThemes.JADE_GREEN:
-      return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
+    case GrowiThemes.FUTURE:
+      return <ThemeFuture>{children}</ThemeFuture>;
+    case GrowiThemes.HALLOWEEN:
+      return <ThemeHalloween>{children}</ThemeHalloween>;
+    case GrowiThemes.HUFFLEPUFF:
+      return <ThemeHufflepuff>{children}</ThemeHufflepuff>;
     case GrowiThemes.ISLAND:
     case GrowiThemes.ISLAND:
       return <ThemeIsland>{children}</ThemeIsland>;
       return <ThemeIsland>{children}</ThemeIsland>;
-    case GrowiThemes.SPRING:
-      return <ThemeSpring>{children}</ThemeSpring>;
+    case GrowiThemes.JADE_GREEN:
+      return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
+    case GrowiThemes.KIBELA:
+      return <ThemeKibela>{children}</ThemeKibela>;
+    case GrowiThemes.MONO_BLUE:
+      return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
     case GrowiThemes.NATURE:
     case GrowiThemes.NATURE:
       return <ThemeNature>{children}</ThemeNature>;
       return <ThemeNature>{children}</ThemeNature>;
+    case GrowiThemes.SPRING:
+      return <ThemeSpring>{children}</ThemeSpring>;
     case GrowiThemes.WOOD:
     case GrowiThemes.WOOD:
       return <ThemeWood>{children}</ThemeWood>;
       return <ThemeWood>{children}</ThemeWood>;
-    case GrowiThemes.MONO_BLUE:
-      return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
     default:
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
       return <ThemeDefault>{children}</ThemeDefault>;
   }
   }

+ 17 - 0
packages/app/src/interfaces/editor-methods.ts

@@ -0,0 +1,17 @@
+export interface IEditorMethods {
+  forceToFocus: () => void,
+  setValue: (newValue: string) => void,
+  setGfmMode: (bool: boolean) => void,
+  setCaretLine: (line: number) => void,
+  setScrollTopByLine: (line: number) => void,
+  getStrFromBol(): void,
+  getStrToEol: () => void,
+  getStrFromBolToSelectedUpperPos: () => void,
+  replaceBolToCurrentPos: (text: string) => void,
+  replaceLine: (text: string) => void,
+  insertText: (text: string) => void,
+  insertLinebreak: () => void,
+  dispatchSave: () => void,
+  dispatchPasteFiles: (event: Event) => void,
+  getNavbarItems: () => JSX.Element[],
+}

+ 15 - 4
packages/app/src/interfaces/page-operation.ts

@@ -6,10 +6,21 @@ export const PageActionType = {
   Revert: 'Revert',
   Revert: 'Revert',
   NormalizeParent: 'NormalizeParent',
   NormalizeParent: 'NormalizeParent',
 } as const;
 } as const;
-export type PageActionType = typeof PageActionType[keyof typeof PageActionType]
-export type IPageOperationProcessData = Partial<{
-  [key in PageActionType]: {isProcessable: boolean}
-}>
+export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
+
+export const PageActionStage = {
+  Main: 'Main',
+  Sub: 'Sub',
+} as const;
+export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage];
+
+export type IPageOperationProcessData = {
+  [key in PageActionType]?: {
+    [PageActionStage.Main]?: { isProcessable: boolean },
+    [PageActionStage.Sub]?: { isProcessable: boolean },
+  }
+}
+
 export type IPageOperationProcessInfo = {
 export type IPageOperationProcessInfo = {
   [pageId: string]: IPageOperationProcessData,
   [pageId: string]: IPageOperationProcessData,
 }
 }

+ 4 - 0
packages/app/src/interfaces/services/renderer.ts

@@ -1,3 +1,5 @@
+import { HastNode } from 'hast-util-select';
+
 import { XssOptionConfig } from '~/services/xss/xssOption';
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 
 // export type GrowiHydratedEnv = {
 // export type GrowiHydratedEnv = {
@@ -18,3 +20,5 @@ export type RendererConfig = {
   plantumlUri: string | null,
   plantumlUri: string | null,
   blockdiagUri: string | null,
   blockdiagUri: string | null,
 } & XssOptionConfig;
 } & XssOptionConfig;
+
+export type RehypePlugin = (option: any) => (node: HastNode) => void

+ 4 - 0
packages/app/src/interfaces/share-link.ts

@@ -0,0 +1,4 @@
+// Todo: specify more detailed Type
+export type IResShareLinkList = {
+  shareLinksResult: any[],
+};

+ 5 - 0
packages/app/src/interfaces/theme.ts

@@ -16,3 +16,8 @@ export const GrowiThemes = {
   WOOD: 'wood',
   WOOD: 'wood',
 } as const;
 } as const;
 export type GrowiThemes = typeof GrowiThemes[keyof typeof GrowiThemes];
 export type GrowiThemes = typeof GrowiThemes[keyof typeof GrowiThemes];
+
+export const PrismThemes = {
+  OneLight: 'one-light',
+} as const;
+export type PrismThemes = typeof PrismThemes[keyof typeof PrismThemes];

+ 2 - 1
packages/app/src/linter-checker/test.scss

@@ -1,7 +1,8 @@
 /*
 /*
  * VSCode の Stylelint 設定チェック方法
  * VSCode の Stylelint 設定チェック方法
  *
  *
- * 1. .stylelintrc.json ファイル中の `src/linter-checker/test.scss` 行を削除
+ * 1. .stylelintrc.json ファイル中の `src/linter-checker/test.scss` 行を削除し、
+ *    このファイルを VSCode 上で開き直す
  *
  *
  * 2. VSCode で以下のエラーが表示されていることを確認
  * 2. VSCode で以下のエラーが表示されていることを確認
  *   - color で stylelint(order/properties-order)
  *   - color で stylelint(order/properties-order)

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