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

Merge branch 'support/apply-nextjs-2' of https://github.com/weseek/growi into support/intallerPage-next-layout

Shunm634-source 3 лет назад
Родитель
Сommit
36ade84c40
78 измененных файлов с 653 добавлено и 247 удалено
  1. 1 1
      .devcontainer/devcontainer.json
  2. 8 13
      .vscode/settings.json
  3. 20 1
      CHANGELOG.md
  4. 1 1
      lerna.json
  5. 1 1
      package.json
  6. 0 0
      packages/app/_obsolete/src/client/plugin.js
  7. 2 2
      packages/app/docker/README.md
  8. 1 0
      packages/app/next.config.js
  9. 10 9
      packages/app/package.json
  10. 1 0
      packages/app/public/static/locales/en_US/translation.json
  11. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  12. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  13. 2 2
      packages/app/src/client/services/AppContainer.js
  14. 1 5
      packages/app/src/client/util/locale-utils.ts
  15. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  16. 1 1
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  17. 1 1
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  18. 3 5
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  19. 1 1
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  20. 2 4
      packages/app/src/components/Admin/Users/ExternalAccountTable.jsx
  21. 18 0
      packages/app/src/components/DescendantsPageListModal.module.scss
  22. 3 1
      packages/app/src/components/DescendantsPageListModal.tsx
  23. 1 1
      packages/app/src/components/InstallerForm.jsx
  24. 4 21
      packages/app/src/components/Me/BasicInfoSettings.tsx
  25. 2 9
      packages/app/src/components/Me/ExternalAccountLinkedMe.jsx
  26. 1 1
      packages/app/src/components/Me/UserSettings.tsx
  27. 1 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  28. 1 1
      packages/app/src/components/Page.jsx
  29. 0 18
      packages/app/src/components/Page/RedirectedAlert.tsx
  30. 1 1
      packages/app/src/components/PageAccessoriesModal.module.scss
  31. 2 0
      packages/app/src/components/PageAlert/PageAlerts.tsx
  32. 46 0
      packages/app/src/components/PageAlert/PageRedirectedAlert.tsx
  33. 1 9
      packages/app/src/components/PageEditor.tsx
  34. 3 1
      packages/app/src/components/PageEditor/Editor.tsx
  35. 14 12
      packages/app/src/components/PageList/PageListItemL.tsx
  36. 3 3
      packages/app/src/components/PagePathHierarchicalLink.tsx
  37. 6 1
      packages/app/src/components/PageTimeline.tsx
  38. 2 2
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  39. 1 1
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  40. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  41. 1 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  42. 7 7
      packages/app/src/components/Theme/ThemeFuture.module.scss
  43. 8 0
      packages/app/src/components/Theme/ThemeFuture.tsx
  44. 7 7
      packages/app/src/components/Theme/ThemeHalloween.module.scss
  45. 8 0
      packages/app/src/components/Theme/ThemeHalloween.tsx
  46. 13 12
      packages/app/src/components/Theme/ThemeHufflepuff.module.scss
  47. 8 0
      packages/app/src/components/Theme/ThemeHufflepuff.tsx
  48. 7 7
      packages/app/src/components/Theme/ThemeKibela.module.scss
  49. 8 0
      packages/app/src/components/Theme/ThemeKibela.tsx
  50. 21 9
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  51. 0 4
      packages/app/src/interfaces/services/renderer.ts
  52. 2 1
      packages/app/src/linter-checker/test.scss
  53. 2 1
      packages/app/src/pages/[[...path]].page.tsx
  54. 17 7
      packages/app/src/pages/admin/[[...path]].page.tsx
  55. 168 0
      packages/app/src/pages/me.page.tsx
  56. 5 6
      packages/app/src/pages/utils/objectid-transformer.ts
  57. 1 1
      packages/app/src/server/routes/index.js
  58. 0 8
      packages/app/src/server/routes/me.js
  59. 3 5
      packages/app/src/services/renderer/rehype-plugins/add-class.ts
  60. 35 0
      packages/app/src/services/renderer/rehype-plugins/relative-links.ts
  61. 20 17
      packages/app/src/services/renderer/renderer.ts
  62. 4 0
      packages/app/src/stores/context.tsx
  63. 30 0
      packages/app/src/stores/page-redirect.tsx
  64. 19 5
      packages/app/src/stores/renderer.tsx
  65. 23 4
      packages/app/src/styles/_wiki.scss
  66. 1 1
      packages/codemirror-textlint/package.json
  67. 1 1
      packages/core/package.json
  68. 1 0
      packages/core/src/index.ts
  69. 27 0
      packages/core/src/test/util/objectid-utils.test.ts
  70. 21 0
      packages/core/src/utils/objectid-utils.ts
  71. 2 3
      packages/core/src/utils/page-path-utils.ts
  72. 1 1
      packages/plugin-attachment-refs/package.json
  73. 1 1
      packages/plugin-lsx/package.json
  74. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  75. 1 1
      packages/slack/package.json
  76. 2 2
      packages/slackbot-proxy/package.json
  77. 2 2
      packages/ui/package.json
  78. 5 0
      yarn.lock

+ 1 - 1
.devcontainer/devcontainer.json

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

+ 8 - 13
.vscode/settings.json

@@ -3,25 +3,20 @@
 
   "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,
+  "less.validate": false,
   "scss.validate": false,
-  "[css]": {
-    "editor.formatOnSave": true
-  },
-  "[scss]": {
-    "editor.formatOnSave": true
-  },
 
-  // for vscode-eslint
-  "[javascript]": {
-    "editor.formatOnSave": false
-  },
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": true,
-    "source.fixAll.markdownlint": true
+    "source.fixAll.markdownlint": true,
+    "source.fixAll.stylelint": true
   },
+
   "githubPullRequests.ignoredPullRequestBranches": [
     "master"
   ]

+ 20 - 1
CHANGELOG.md

@@ -1,9 +1,28 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.1.1...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.2...HEAD)
 
 *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

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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


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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.1.1`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.1/docker/Dockerfile)
-* [`5.1.1-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.1/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-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)

+ 1 - 0
packages/app/next.config.js

@@ -29,6 +29,7 @@ const setupTranspileModules = () => {
     'decode-named-character-reference',
     'hastscript',
     'html-void-elements',
+    'is-absolute-url',
     'longest-streak',
     'property-information',
     'space-separated-tokens',

+ 10 - 9
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -37,7 +37,7 @@
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
     "test:ci": "cross-env NODE_ENV=test jest",
-    "prelint:eslint": "yarn resources:plugin",
+    "// prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "reg:run": "reg-suit run",
     "//// misc": "",
@@ -63,12 +63,12 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.2-RC.0",
-    "@growi/core": "^5.1.2-RC.0",
-    "@growi/plugin-attachment-refs": "^5.1.2-RC.0",
-    "@growi/plugin-lsx": "^5.1.2-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.2-RC.0",
-    "@growi/slack": "^5.1.2-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/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -115,6 +115,7 @@
     "i18next-chained-backend": "^3.0.2",
     "i18next-http-backend": "^1.4.1",
     "i18next-localstorage-backend": "^3.1.3",
+    "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
     "md5": "^2.2.1",
@@ -190,7 +191,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.2-RC.0",
+    "@growi/ui": "^5.1.3-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -19,6 +19,7 @@
   "Move/Rename": "Move/Rename",
   "Redirected": "Redirected",
   "Unlinked": "Unlinked",
+  "unlink_redirection": "Unlink redirection",
   "Done": "Done",
   "Cancel": "Cancel",
   "Create": "Create",

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

@@ -19,6 +19,7 @@
   "Move/Rename": "移動/名前変更",
   "Redirected": "リダイレクトされました",
   "Unlinked": "リダイレクト削除",
+  "unlink_redirection": "リダイレクト削除",
   "Done": "完了",
   "Cancel": "キャンセル",
   "Create": "作成",

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

@@ -20,6 +20,7 @@
 	"Move/Rename": "移动/重命名",
 	"Redirected": "重定向",
 	"Unlinked": "Unlinked",
+  "unlink_redirection": "取消链接重定向",
   "Done": "Done",
   "Cancel": "取消",
 	"Create": "创建",

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

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

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

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

+ 1 - 1
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -40,7 +40,7 @@ class ExportArchiveDataPage extends React.Component {
     this.exportingRequestedHandler = this.exportingRequestedHandler.bind(this);
   }
 
-  async UNSAFE_UNSAFE_componentWillMount() {
+  async UNSAFE_componentWillMount() {
     // TODO:: use apiv3.get
     // eslint-disable-next-line no-unused-vars
     const [{ collections }, { status }] = await Promise.all([

+ 1 - 1
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -94,7 +94,7 @@ class ImportForm extends React.Component {
     return Object.keys(this.state.collectionNameToFileNameMap);
   }
 
-  UNSAFE_UNSAFE_componentWillMount() {
+  UNSAFE_componentWillMount() {
     this.setupWebsocketEventHandler();
   }
 

+ 1 - 1
packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -31,7 +31,7 @@ class GrowiArchiveSection extends React.Component {
     this.renderDefferentVersionAlert = this.renderDefferentVersionAlert.bind(this);
   }
 
-  async UNSAFE_UNSAFE_componentWillMount() {
+  async UNSAFE_componentWillMount() {
     // get uploaded file status
     const res = await apiv3Get('/import/status');
 

+ 3 - 5
packages/app/src/components/Admin/ManageExternalAccount.jsx

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

+ 1 - 1
packages/app/src/components/Admin/Security/ShareLinkSetting.jsx

@@ -54,7 +54,7 @@ class ShareLinkSetting extends React.Component {
     this.switchDisableLinkSharing = this.switchDisableLinkSharing.bind(this);
   }
 
-  UNSAFE_UNSAFE_componentWillMount() {
+  UNSAFE_componentWillMount() {
     this.getShareLinkList(1);
   }
 

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

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

+ 18 - 0
packages/app/src/components/DescendantsPageListModal.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;
+  }
+}

+ 3 - 1
packages/app/src/components/DescendantsPageListModal.tsx

@@ -17,6 +17,8 @@ import ExpandOrContractButton from './ExpandOrContractButton';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 
+import styles from './DescendantsPageListModal.module.scss';
+
 const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
   const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
   return <DescendantsPageList {...props}/>;
@@ -92,7 +94,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
       isOpen={isOpened}
       toggle={close}
       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}>
         <CustomNavTab

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

@@ -96,7 +96,7 @@ class InstallerForm extends React.Component {
                           data-testid={`dropdownLanguageMenu-${locale}`}
                           className="dropdown-item"
                           type="button"
-                          onClick={() => { i18next.changeLanguage(meta.id) }}
+                          onClick={() => { i18next.changeLanguage(locale) }}
                         >
                           {fixedT('meta.display_name')}
                         </button>

+ 4 - 21
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -2,20 +2,14 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { localeMetadatas } from '~/client/util/i18n';
+import { useRegistrationWhiteList } from '~/stores/context';
 import { usePersonalSettings } from '~/stores/personal-settings';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-type Props = {
-  appContainer: AppContainer,
-}
-
-const BasicInfoSettings = (props: Props) => {
+export const BasicInfoSettings = (): JSX.Element => {
   const { t } = useTranslation();
-  const { appContainer } = props;
+  const { data: registrationWhiteList } = useRegistrationWhiteList();
 
   const {
     data: personalSettingsInfo, mutate: mutatePersonalSettings, sync, updateBasicInfo, error,
@@ -34,9 +28,6 @@ const BasicInfoSettings = (props: Props) => {
     }
   };
 
-
-  const { registrationWhiteList } = appContainer.getConfig();
-
   const changePersonalSettingsHandler = (updateData) => {
     if (personalSettingsInfo == null) {
       return;
@@ -71,7 +62,7 @@ const BasicInfoSettings = (props: Props) => {
             defaultValue={personalSettingsInfo?.email || ''}
             onChange={e => changePersonalSettingsHandler({ email: e.target.value })}
           />
-          {registrationWhiteList.length !== 0 && (
+          {registrationWhiteList != null && registrationWhiteList.length !== 0 && (
             <div className="form-text text-muted">
               {t('page_register.form_help.email')}
               <ul>
@@ -163,11 +154,3 @@ const BasicInfoSettings = (props: Props) => {
     </>
   );
 };
-
-
-/**
- * Wrapper component for using unstated
- */
-const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettings, [AppContainer]);
-
-export default BasicInfoSettingsWrapper;

+ 2 - 9
packages/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -115,7 +114,6 @@ class ExternalAccountLinkedMe extends React.Component {
 
 ExternalAccountLinkedMe.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalExternalAccounts: PropTypes.arrayOf(PropTypes.object),
 };
 
@@ -126,9 +124,4 @@ const ExternalAccountLinkedMeWrapperFC = (props) => {
   return <ExternalAccountLinkedMe t={t} personalExternalAccounts={personalExternalAccountsData} {...props} />;
 };
 
-/**
- * Wrapper component for using unstated
- */
-const ExternalAccountLinkedMeWrapper = withUnstatedContainers(ExternalAccountLinkedMeWrapperFC, [AppContainer]);
-
-export default ExternalAccountLinkedMeWrapper;
+export default ExternalAccountLinkedMeWrapperFC;

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

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import BasicInfoSettings from './BasicInfoSettings';
+import { BasicInfoSettings } from './BasicInfoSettings';
 import ProfileImageSettings from './ProfileImageSettings';
 
 const UserSettings = React.memo((): JSX.Element => {

+ 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`}>
       {/* Brand Logo  */}
       <div className="navbar-brand mr-0">
-        <Link href="/">
+        <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
             <GrowiLogo />
           </a>

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

@@ -204,7 +204,7 @@ export const Page = (props) => {
 
     blinkSectionHeaderAtBoot();
     mutateBlinkedAtBoot(true);
-  }, [mutateBlinkedAtBoot]);
+  }, [isBlinkedAtBoot, mutateBlinkedAtBoot]);
 
   // // set handler to open DrawioModal
   // useEffect(() => {

+ 0 - 18
packages/app/src/components/Page/RedirectedAlert.tsx

@@ -1,18 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-const RedirectedAlert = React.memo((): JSX.Element => {
-  const { t } = useTranslation();
-  const urlParams = new URLSearchParams(window.location.search);
-  const fromPath = urlParams.get('redirectFrom');
-
-  return (
-    <>
-      <strong>{ t('Redirected') }:</strong> { t('page_page.notice.redirected')} <code>{fromPath}</code> {t('page_page.notice.redirected_period')}
-    </>
-  );
-});
-RedirectedAlert.displayName = 'RedirectedAlert';
-
-export default RedirectedAlert;

+ 1 - 1
packages/app/src/components/PageAccessoriesModal.module.scss

@@ -1,4 +1,4 @@
-.grw-page-accessories-modal {
+.grw-page-accessories-modal :global {
   .modal-header {
     button.close {
       margin: auto 0rem auto auto;

+ 2 - 0
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -7,6 +7,7 @@ import { useIsNotFound } from '~/stores/context';
 import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
+import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 
 // dynamic import because TrashPageAlert uses localStorageMiddleware
@@ -25,6 +26,7 @@ export const PageAlerts = (): JSX.Element => {
         <TrashPageAlert />
         <PageStaleAlert />
         <OldRevisionAlert />
+        <PageRedirectedAlert />
       </div>
     </div>
   );

+ 46 - 0
packages/app/src/components/PageAlert/PageRedirectedAlert.tsx

@@ -0,0 +1,46 @@
+import React, { useState, useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { toastError } from '~/client/util/apiNotification';
+import { useRedirectFrom } from '~/stores/page-redirect';
+
+export const PageRedirectedAlert = React.memo((): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: redirectFrom, unlink } = useRedirectFrom();
+
+  const [isUnlinked, setIsUnlinked] = useState(false);
+
+  const unlinkButtonClickHandler = useCallback(async() => {
+    try {
+      await unlink();
+      setIsUnlinked(true);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [unlink]);
+
+  if (redirectFrom == null) {
+    return <></>;
+  }
+
+  if (isUnlinked) {
+    return (<div className="alert alert-info d-edit-none py-3 px-4">
+      <strong>{ t('Unlinked') }: </strong> { t('page_page.notice.unlinked') }
+    </div>);
+  }
+
+  return (
+    <div className="alert alert-pink d-edit-none py-3 px-4 d-flex align-items-center justify-content-between">
+      <span>
+        <strong>{ t('Redirected') }:</strong> { t('page_page.notice.redirected')} <code>{redirectFrom}</code> {t('page_page.notice.redirected_period')}
+      </span>
+      <button type="button" id="unlink-page-button" onClick={unlinkButtonClickHandler} className="btn btn-outline-dark btn-sm float-right">
+        <i className="ti ti-unlink" aria-hidden="true"></i>
+        {t('unlink_redirection')}
+      </button>
+    </div>
+  );
+});
+PageRedirectedAlert.displayName = 'PageRedirectedAlert';

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

@@ -402,20 +402,12 @@ const PageEditor = (props: Props): JSX.Element => {
   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;
-
   return (
     <div className="d-flex flex-wrap">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
-        <EditorAny
+        <Editor
           ref={editorRef}
           value={markdown}
-          isMobile={isMobile}
           isUploadable={isUploadable}
           isUploadableFile={isUploadableFile}
           isTextlintEnabled={isTextlintEnabled}

+ 3 - 1
packages/app/src/components/PageEditor/Editor.tsx

@@ -27,9 +27,11 @@ type EditorPropsType = {
   noCdn?: boolean,
   isUploadable?: boolean,
   isUploadableFile?: boolean,
-  onChange?: () => void,
+  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,

+ 14 - 12
packages/app/src/components/PageList/PageListItemL.tsx

@@ -8,6 +8,7 @@ import { DevidedPagePath } from '@growi/core';
 import { UserPicture, PageListMeta } from '@growi/ui';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 import { CustomInput } from 'reactstrap';
 import urljoin from 'url-join';
@@ -201,18 +202,19 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                 <span className="h5 mb-0">
                   {/* 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">
-                    {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>
               </Clamp>

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

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

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

@@ -1,6 +1,7 @@
 import React, { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
@@ -54,7 +55,11 @@ export const PageTimeline = (): JSX.Element => {
         return (
           <div className="timeline-body" key={`key-${page._id}`}>
             <div className={`card card-timeline ${styles['card-timeline']}`}>
-              <div className="card-header"><a href={page.path}>{page.path}</a></div>
+              <div className="card-header">
+                <Link href={page.path} prefetch={false}>
+                  <a>{page.path}</a>
+                </Link>
+              </div>
               <div className="card-body">
                 <RevisionLoader
                   lazy

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

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

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

@@ -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">
                   <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{nodePath.basename(page.path ?? '') || '/'}</p>
                 </a>

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

@@ -66,7 +66,7 @@ const LargePageItem = memo(({ page }: PageItemProps): JSX.Element => {
       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">
           {tag.name}
         </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;
 
   return (
-    <Link href={href}>
+    <Link href={href} prefetch={false}>
       <a className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
         <i className="material-icons">{iconName}</i>
       </a>

+ 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;
 $themecolor: #16282d;
 $accentcolor: #00fff5;
 
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   $bgcolor-global: $themecolor;
   $bgcolor-inline-code: #1f1f22; //optional
@@ -83,13 +83,13 @@ html[dark] {
   // admin theme box
   $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
   .btn-group.grw-page-editor-mode-manager {
     .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;
 $themelight: #f0f8ff;
@@ -33,8 +34,7 @@ $light: lighten($secondary, 10%);
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   $bgcolor-global: #050000;
   $bgcolor-inline-code: #1f1f22; //optional
@@ -105,13 +105,13 @@ html[dark] {
   // admin theme box
   $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
   .btn-group.grw-page-editor-mode-manager {
     .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
 //
@@ -19,7 +20,7 @@
 
 //== Light Mode
 //
-html[light] {
+.theme[data-color-scheme='light'] :global {
   // Theme colors
   $themecolor: #eaab20;
   $themelight: #efe2cf;
@@ -91,16 +92,16 @@ html[light] {
   // admin theme box
   $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
   .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.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
   $themecolor: #eaab20;
   $themedark: #3d3f38;
@@ -234,8 +235,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   // Navs
   .nav-tabs {
@@ -260,11 +261,11 @@ html[dark] {
 
   // Button
   .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.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);
 $themelight: #f4f5f6;
@@ -27,8 +28,7 @@ $lightthemecolor: rgba(181, 203, 247, 0.61);
 }
 
 // Light Mode
-html[light],
-html[dark] {
+.theme :global {
   // Background colors
   $bgcolor-navbar: white;
   $bgcolor-navbar-active: $bgcolor-theme;
@@ -98,13 +98,13 @@ html[dark] {
   // Sidebar list group
   $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
   .grw-page-editor-mode-manager {
     .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 ThemeDefault = dynamic(() => import('../ThemeDefault'));
 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 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 ThemeSpring = dynamic(() => import('../ThemeSpring'));
 const ThemeWood = dynamic(() => import('../ThemeWood'));
-const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
 
 
 type Props = {
@@ -34,18 +38,26 @@ export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
       return <ThemeChristmas>{children}</ThemeChristmas>;
     case GrowiThemes.FIRE_RED:
       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:
       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:
       return <ThemeNature>{children}</ThemeNature>;
+    case GrowiThemes.SPRING:
+      return <ThemeSpring>{children}</ThemeSpring>;
     case GrowiThemes.WOOD:
       return <ThemeWood>{children}</ThemeWood>;
-    case GrowiThemes.MONO_BLUE:
-      return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
   }

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

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

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

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

+ 2 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -34,6 +34,7 @@ import { PageModel, PageDocument } from '~/server/models/page';
 import { PageRedirectModel } from '~/server/models/page-redirect';
 import { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
+import { useRedirectFrom } from '~/stores/page-redirect';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSelectedGrant,
 } from '~/stores/ui';
@@ -187,12 +188,12 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
 
   // page
-  useCurrentPagePath(props.currentPathname);
   useIsLatestRevision(props.isLatestRevision);
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   useIsForbidden(props.isForbidden);
   useIsNotFound(props.isNotFound);
   useIsNotCreatable(props.IsNotCreatable);
+  useRedirectFrom(props.redirectFrom);
   // useIsTrashPage(_isTrashPage(props.currentPagePath));
   // useShared();
   // useShareLinkId(props.shareLinkId);

+ 17 - 7
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -54,6 +54,7 @@ const NotificationSetting = dynamic(() => import('../../components/Admin/Notific
 const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
 const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
 const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
+const ManageExternalAccount = dynamic(() => import('../../components/Admin/ManageExternalAccount'), { ssr: false });
 const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage'), { ssr: false });
 const ElasticsearchManagement = dynamic(() => import('../../components/Admin/ElasticsearchManagement/ElasticsearchManagement'), { ssr: false });
 // named export
@@ -84,8 +85,8 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
   const { t } = useTranslation('admin');
   const router = useRouter();
-  const path = router.query.path || 'home';
-  const name = Array.isArray(path) ? path[0] : path;
+  const { path } = router.query;
+  const pagePathKeys: string[] = Array.isArray(path) ? path : ['home'];
 
   const adminPagesMap = {
     home: {
@@ -116,7 +117,6 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     importer: {
       title: useCustomTitle(props, t('Import Data')),
       component: <DataImportPageContents />,
-
     },
     export: {
       title: useCustomTitle(props, t('Export Archive Data')),
@@ -141,6 +141,10 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     users: {
       title: useCustomTitle(props, t('User_Management')),
       component: <UserManagement />,
+      'external-accounts': {
+        title: useCustomTitle(props, t('external_account_management')),
+        component: <ManageExternalAccount />,
+      },
     },
     'user-groups': {
       title: useCustomTitle(props, t('UserGroup Management')),
@@ -156,8 +160,14 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
   };
 
-  const content = adminPagesMap[name];
-  const title = content.title;
+  const getTargetPageToRender = (pagesMap, keys) => {
+    return keys.reduce((pagesMap, key) => {
+      return pagesMap[key];
+    }, pagesMap);
+  };
+
+  const targetPage: {title: string, component: JSX.Element} = getTargetPageToRender(adminPagesMap, pagePathKeys);
+  const title = targetPage.title;
 
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   // useIsMailerSetup(props.isMailerSetup);
@@ -235,8 +245,8 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
   return (
     <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <AdminLayout title={title} selectedNavOpt={name}>
-        {content.component}
+      <AdminLayout title={title} selectedNavOpt={pagePathKeys[0]}>
+        {targetPage.component}
       </AdminLayout>
     </Provider>
   );

+ 168 - 0
packages/app/src/pages/me.page.tsx

@@ -0,0 +1,168 @@
+import React from 'react';
+
+import {
+  IUser, IUserHasId,
+} from '@growi/core';
+import { model as mongooseModel } from 'mongoose';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import { UserUISettingsModel } from '~/server/models/user-ui-settings';
+import {
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
+} from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+
+
+import { BasicLayout } from '../components/Layout/BasicLayout';
+import {
+  useCurrentUser,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault,
+  useRegistrationWhiteList,
+} from '../stores/context';
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+} from './utils/commons';
+
+
+const logger = loggerFactory('growi:pages:me');
+
+type Props = CommonProps & {
+  currentUser: IUser,
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isSearchScopeChildrenAsDefault: boolean,
+  userUISettings?: IUserUISettings
+  sidebarConfig: ISidebarConfig,
+
+  // config
+  registrationWhiteList: string[],
+};
+
+const MePage: NextPage<Props> = (props: Props) => {
+  useCurrentUser(props.currentUser ?? null);
+
+  useRegistrationWhiteList(props.registrationWhiteList);
+
+  // commons
+  useCsrfToken(props.csrfToken);
+
+  // // UserUISettings
+  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+
+  // // page
+  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+  const { t } = useTranslation();
+
+  const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
+
+  return (
+    <>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')}>
+
+        <header className="py-3">
+          <div className="container-fluid">
+            <h1 className="title">{t('User Settings')}</h1>
+          </div>
+        </header>
+
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+
+        <div id="main" className='main'>
+          <div id="content-main" className="content-main grw-container-convertible">
+            <PersonalSettings />
+          </div>
+        </div>
+
+      </BasicLayout>
+    </>
+  );
+};
+
+async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
+
+  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+  if (userUISettings != null) {
+    props.userUISettings = userUISettings.toObject();
+  }
+}
+
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    searchService,
+    configManager,
+  } = crowi;
+
+  props.isSearchServiceConfigured = searchService.isConfigured;
+  props.isSearchServiceReachable = searchService.isReachable;
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+
+  props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
+
+  props.sidebarConfig = {
+    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+}
+
+// /**
+//  * for Server Side Translations
+//  * @param context
+//  * @param props
+//  * @param namespacesRequired
+//  */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+
+  const result = await getServerSideCommonProps(context);
+
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  await injectUserUISettings(context, props);
+  await injectServerConfigurations(context, props);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default MePage;

+ 5 - 6
packages/app/src/pages/utils/objectid-transformer.ts

@@ -1,4 +1,5 @@
 // !!! Do NOT import 'mongoose' to reduce bundle size !!!
+import { objectIdUtils } from '@growi/core';
 import ObjectId from 'bson-objectid';
 import superjson from 'superjson';
 
@@ -9,13 +10,11 @@ export const registerTransformerForObjectId = (): void => {
         if (v == null) {
           return false;
         }
-        if (typeof v === 'string') {
-          return ObjectId.isValid(v);
-        }
-        if (typeof v.toHexString === 'function') {
-          return ObjectId.isValid(v.toHexString());
+        // Only evaluate types for string and ObjectId
+        if (typeof v !== 'string' && typeof v.toHexString !== 'function') {
+          return false;
         }
-        return false;
+        return objectIdUtils.isValidObjectId(v);
       },
       serialize: v => (typeof v === 'string' ? v : v.toHexString()),
       deserialize: v => v,

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -207,7 +207,7 @@ module.exports = function(crowi, app) {
   // app.get('/tags'                     , loginRequired, tag.showPage);
   app.get('/tags', loginRequired, next.delegateToNext);
 
-  app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, me.index);
+  app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   // external-accounts
   // my in-app-notifications
   app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, allInAppNotifications.list);

+ 0 - 8
packages/app/src/server/routes/me.js

@@ -100,14 +100,6 @@ module.exports = function(crowi, app) {
       });
   };
 
-  actions.index = async function(req, res) {
-    const User = crowi.model('User');
-    const userData = await User.findById(req.user.id).populate({ path: 'imageAttachment', select: 'filePathProxied' });
-    const renderVars = {};
-    renderVars.user = userData;
-    return res.render('me/index', renderVars);
-  };
-
   actions.externalAccounts = {};
   actions.externalAccounts.list = function(req, res) {
     const userData = req.user;

+ 3 - 5
packages/app/src/services/renderer/rehype-plugins/add-class.ts

@@ -1,9 +1,7 @@
 // See: https://github.com/martypdx/rehype-add-classes for the original implementation.
 // Re-implemeted in TypeScript.
-
 import { selectAll, HastNode, Element } from 'hast-util-select';
-
-import { RehypePlugin } from '~/interfaces/services/renderer';
+import { Plugin } from 'unified';
 
 export type SelectorName = string; // e.g. 'h1'
 export type ClassName = string; // e.g. 'header'
@@ -32,8 +30,8 @@ const adder = (entry: AdditionsEntry) => {
   return (node: HastNode) => selectAll(selectorName, node).forEach(writer);
 };
 
-export const addClass: RehypePlugin = (additions) => {
+export const addClass: Plugin<[Additions]> = (additions) => {
   const adders = Object.entries(additions).map(adder);
 
-  return node => adders.forEach(a => a(node));
+  return node => adders.forEach(a => a(node as HastNode));
 };

+ 35 - 0
packages/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -0,0 +1,35 @@
+import { selectAll, HastNode } from 'hast-util-select';
+import isAbsolute from 'is-absolute-url';
+import { Plugin } from 'unified';
+
+type RelativeLinksPluginParams = {
+  pagePath?: string,
+}
+
+export const relativeLinks: Plugin<[RelativeLinksPluginParams]> = (options = {}) => {
+  return (tree) => {
+    if (options.pagePath == null) {
+      return;
+    }
+
+    const pagePath = options.pagePath;
+    const anchors = selectAll('a[href]', tree as HastNode);
+
+    anchors.forEach((anchor) => {
+      if (anchor.properties == null) {
+        return;
+      }
+
+      const href = anchor.properties.href;
+      if (href == null || typeof href !== 'string' || isAbsolute(href)) {
+        return;
+      }
+
+      // generate relative pathname
+      const baseUrl = new URL(pagePath, 'https://example.com');
+      const relativeUrl = new URL(href, baseUrl);
+
+      anchor.properties.href = relativeUrl.pathname;
+    });
+  };
+};

+ 20 - 17
packages/app/src/services/renderer/renderer.tsx → packages/app/src/services/renderer/renderer.ts

@@ -1,7 +1,7 @@
 import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
-import sanitize, { defaultSchema } from 'rehype-sanitize';
+import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
@@ -13,9 +13,11 @@ import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { RendererConfig } from '~/interfaces/services/renderer';
-import { addClass } from '~/services/renderer/rehype-plugins/add-class';
 import loggerFactory from '~/utils/logger';
 
+import { addClass } from './rehype-plugins/add-class';
+import { relativeLinks } from './rehype-plugins/relative-links';
+
 // import CsvToTable from './PreProcessor/CsvToTable';
 // import EasyGrid from './PreProcessor/EasyGrid';
 // import Linker from './PreProcessor/Linker';
@@ -212,21 +214,21 @@ const logger = loggerFactory('growi:util:GrowiRenderer');
 
 export type RendererOptions = Partial<ReactMarkdownOptions>;
 
-export interface ReactMarkdownOptionsGenerator {
-  (config: RendererConfig): RendererOptions
-}
 
-const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+const generateCommonOptions = (pagePath: string|undefined, config: RendererConfig): RendererOptions => {
   return {
     remarkPlugins: [gfm],
     rehypePlugins: [
       slug,
+      [relativeLinks, { pagePath }],
       raw,
       [sanitize, {
-        ...defaultSchema,
+        ...sanitizeDefaultSchema,
         attributes: {
-          ...defaultSchema.attributes,
-          '*': ['className', 'class'],
+          ...sanitizeDefaultSchema.attributes,
+          '*': sanitizeDefaultSchema.attributes != null
+            ? sanitizeDefaultSchema.attributes['*'].concat('class', 'className')
+            : ['class', 'className'],
         },
       }],
       [addClass, {
@@ -241,11 +243,12 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
 };
 
 export const generateViewOptions = (
+    pagePath: string,
     config: RendererConfig,
     storeTocNode: (node: HtmlElementNode) => void,
 ): RendererOptions => {
 
-  const options = generateCommonOptions(config);
+  const options = generateCommonOptions(pagePath, config);
 
   const { remarkPlugins, rehypePlugins, components } = options;
 
@@ -310,7 +313,7 @@ export const generateViewOptions = (
 
 export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
 
-  const options = generateCommonOptions(config);
+  const options = generateCommonOptions(undefined, config);
 
   const { remarkPlugins, rehypePlugins } = options;
 
@@ -332,8 +335,8 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   return options;
 };
 
-export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generatePreviewOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
 
   // // Add configurers for preview
   // renderer.addConfigurers([
@@ -348,8 +351,8 @@ export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: Re
   return options;
 };
 
-export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generateCommentPreviewOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
   const { remarkPlugins } = options;
 
   // add remark plugins
@@ -370,8 +373,8 @@ export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (con
   return options;
 };
 
-export const generateOthersOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(config);
+export const generateOthersOptions = (config: RendererConfig): RendererOptions => {
+  const options = generateCommonOptions(undefined, config);
 
   // renderer.addConfigurers([
   //   new TableConfigurer(),

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -128,6 +128,10 @@ export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRRespo
   return useStaticSWR<Nullable<boolean>, Error>('disableLinkSharing', initialData);
 };
 
+export const useRegistrationWhiteList = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
+  return useStaticSWR<Nullable<string[]>, Error>('registrationWhiteList', initialData);
+};
+
 export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
   return useStaticSWR<Nullable<any>, Error>('revisionIdHackmdSynced', initialData);
 };

+ 30 - 0
packages/app/src/stores/page-redirect.tsx

@@ -0,0 +1,30 @@
+import { SWRResponseWithUtils, withUtils } from '@growi/core/src/utils/with-utils';
+import { SWRResponse } from 'swr';
+
+import { apiPost } from '~/client/util/apiv1-client';
+
+import { useCurrentPagePath } from './context';
+import { useStaticSWR } from './use-static-swr';
+
+type RedirectFromUtil = {
+  unlink(): Promise<void>
+}
+export const useRedirectFrom = (initialData?: string): SWRResponseWithUtils<RedirectFromUtil, string> => {
+  const { data: currentPagePath } = useCurrentPagePath();
+  const swrResponse: SWRResponse<string, Error> = useStaticSWR('redirectFrom', initialData);
+  const utils = {
+    unlink: async() => {
+      if (currentPagePath == null) {
+        return;
+      }
+      try {
+        await apiPost('/pages.unlink', { path: currentPagePath });
+        swrResponse.mutate('');
+      }
+      catch (err) {
+        throw err;
+      }
+    },
+  };
+  return withUtils(swrResponse, utils);
+};

+ 19 - 5
packages/app/src/stores/renderer.tsx

@@ -1,14 +1,19 @@
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
+import { RendererConfig } from '~/interfaces/services/renderer';
 import {
-  ReactMarkdownOptionsGenerator, RendererOptions,
+  RendererOptions,
   generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
   generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 
 
-import { useCurrentPageTocNode, useRendererConfig } from './context';
+import { useCurrentPagePath, useCurrentPageTocNode, useRendererConfig } from './context';
+
+interface ReactMarkdownOptionsGenerator {
+  (config: RendererConfig): RendererOptions
+}
 
 // The base hook with common processes
 const _useOptionsBase = (
@@ -33,11 +38,20 @@ const _useOptionsBase = (
 };
 
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
-  const key = 'viewOptions';
-
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: rendererConfig } = useRendererConfig();
   const { mutate: storeTocNode } = useCurrentPageTocNode();
 
-  return _useOptionsBase(key, config => generateViewOptions(config, storeTocNode));
+  const isAllDataValid = currentPagePath != null && rendererConfig != null;
+
+  const key = isAllDataValid
+    ? ['viewOptions', currentPagePath, rendererConfig]
+    : null;
+
+  return useSWRImmutable<RendererOptions, Error>(
+    key,
+    (rendererId, currentPagePath, rendererConfig) => generateViewOptions(currentPagePath, rendererConfig, storeTocNode),
+  );
 };
 
 export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {

+ 23 - 4
packages/app/src/styles/_wiki.scss

@@ -45,6 +45,7 @@
     // style
     border-bottom: solid 1px transparent;
   }
+
   h2 {
     padding-bottom: 0.3em;
     font-size: 1.6em;
@@ -53,22 +54,26 @@
     // style
     border-bottom: solid 1px transparent;
   }
+
   h3 {
     font-size: 1.4em;
     font-weight: bold;
   }
+
   h4 {
     font-size: 1.35em;
     font-weight: normal;
     // style
     @include add-left-border(6px);
   }
+
   h5 {
     font-size: 1.25em;
     font-weight: normal;
     // style
     @include add-left-border(4px);
   }
+
   h6 {
     font-size: 1.2em;
     font-weight: normal;
@@ -80,6 +85,7 @@
     margin: 15px 0;
     font-weight: normal;
   }
+
   blockquote {
     padding: 0 20px;
     margin: 0 0 30px 0;
@@ -117,28 +123,35 @@
     position: relative; // for absolute positioned .code-highlighted-title
   }
 
-  .task-list {
+  .contains-task-list {
     .task-list-item {
       margin: 0 0.2em 0.25em -1.6em;
       list-style-type: none;
     }
-    .task-list-item > .task-list {
+
+    .task-list-item > .contains-task-list {
       margin-left: 30px;
     }
+
     // use awesome-bootstrap-checkbox
     .task-list-item .checkbox input[type='checkbox'] {
+
       // layout
-      + label {
+      +label {
         padding-left: 0.3em;
+
         &:before {
           margin-top: 0.4em;
         }
       }
+
       // styles
       cursor: default;
-      + label {
+
+      +label {
         cursor: default;
         opacity: 1;
+
         &:before,
         &:after {
           cursor: default;
@@ -148,6 +161,7 @@
   }
 
   $ratio: 0.95;
+
   &.comment {
     font-size: 14px;
     line-height: 1.5em;
@@ -172,11 +186,13 @@
       font-size: 1.8em * $ratio;
       line-height: 1.1em * $ratio;
     }
+
     h2 {
       padding-bottom: 0.5em * $ratio;
       font-size: 1.4em * $ratio;
       line-height: 1.225 * $ratio;
     }
+
     h3 {
       font-size: 1.2em * $ratio;
     }
@@ -192,6 +208,7 @@
 
       li {
         line-height: bs.$line-height-base;
+
         pre {
           line-height: bs.$line-height-base;
         }
@@ -199,6 +216,7 @@
     }
 
     .revision-head {
+
       .revision-head-link,
       .revision-head-edit-button {
         margin-left: 0.5em * $ratio;
@@ -224,6 +242,7 @@
 
 // mobile
 .page-mobile .wiki .revision-head {
+
   .revision-head-link,
   .revision-head-edit-button {
     opacity: 0.3;

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 0
packages/core/src/index.ts

@@ -7,6 +7,7 @@ export const customTagUtils = _customTagUtils;
 
 // export utils with namespace
 export * as templateChecker from './utils/template-checker';
+export * as objectIdUtils from './utils/objectid-utils';
 export * as pagePathUtils from './utils/page-path-utils';
 export * as pathUtils from './utils/path-utils';
 export * as pageUtils from './utils/page-utils';

+ 27 - 0
packages/core/src/test/util/objectid-utils.test.ts

@@ -0,0 +1,27 @@
+import ObjectId from 'bson-objectid';
+
+import { isValidObjectId } from '../../utils/objectid-utils';
+
+describe('isValidObjectId', () => {
+
+  /* eslint-disable indent */
+  describe.each`
+    arg                                           | expected
+    ${undefined}                                  | ${false}
+    ${null}                                       | ${false}
+    ${'geeks'}                                    | ${false}
+    ${'toptoptoptop'}                             | ${false}
+    ${'geeksfogeeks'}                             | ${false}
+    ${'594ced02ed345b2b049222c5'}                 | ${true}
+    ${new ObjectId('594ced02ed345b2b049222c5')}   | ${true}
+  `('should return $expected', ({ arg, expected }) => {
+    test(`when the argument is '${arg}'`, async() => {
+      // when:
+      const result = isValidObjectId(arg);
+
+      // then:
+      expect(result).toBe(expected);
+    });
+  });
+
+});

+ 21 - 0
packages/core/src/utils/objectid-utils.ts

@@ -0,0 +1,21 @@
+import ObjectId from 'bson-objectid';
+
+import { isServer } from './browser-utils';
+
+// Workaround to avoid https://github.com/williamkapke/bson-objectid/issues/50
+if (isServer()) {
+  global._Buffer = Buffer;
+}
+
+export function isValidObjectId(id: string | ObjectId | null | undefined): boolean {
+  if (id == null) {
+    return false;
+  }
+
+  // implement according to https://www.geeksforgeeks.org/how-to-check-if-a-string-is-valid-mongodb-objectid-in-node-js/
+  if (typeof id === 'string') {
+    return ObjectId.isValid(id) && new ObjectId(id).toHexString() === id;
+  }
+
+  return ObjectId.isValid(id);
+}

+ 2 - 3
packages/core/src/utils/page-path-utils.ts

@@ -1,9 +1,8 @@
 import nodePath from 'path';
 
-import ObjectId from 'bson-objectid';
 import escapeStringRegexp from 'escape-string-regexp';
 
-
+import { isValidObjectId } from './objectid-utils';
 import { addTrailingSlash } from './path-utils';
 
 /**
@@ -28,7 +27,7 @@ export const isUsersTopPage = (path: string): boolean => {
  */
 export const isPermalink = (path: string): boolean => {
   const pageIdStr = path.substring(1);
-  return ObjectId.isValid(pageIdStr);
+  return isValidObjectId(pageIdStr);
 };
 
 /**

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.1.2-slackbot-proxy.0",
+  "version": "5.1.3-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.2-RC.0",
+    "@growi/slack": "^5.1.3-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.1.2-RC.0",
+  "version": "5.1.3-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [
@@ -21,7 +21,7 @@
     "test": "jest --verbose"
   },
   "dependencies": {
-    "@growi/core": "^5.1.2-RC.0"
+    "@growi/core": "^5.1.3-RC.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 5 - 0
yarn.lock

@@ -11606,6 +11606,11 @@ ipaddr.js@1.9.1:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
+is-absolute-url@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-4.0.1.tgz#16e4d487d4fded05cfe0685e53ec86804a5e94dc"
+  integrity sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==
+
 is-accessor-descriptor@^0.1.6:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"