jam411 пре 3 година
родитељ
комит
c3485e3b60
70 измењених фајлова са 535 додато и 342 уклоњено
  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. 9 9
      packages/app/package.json
  9. 2 2
      packages/app/src/client/services/AppContainer.js
  10. 1 5
      packages/app/src/client/util/locale-utils.ts
  11. 2 4
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  12. 2 4
      packages/app/src/components/Admin/Users/ExternalAccountTable.jsx
  13. 18 0
      packages/app/src/components/DescendantsPageListModal.module.scss
  14. 3 1
      packages/app/src/components/DescendantsPageListModal.tsx
  15. 3 3
      packages/app/src/components/InstallerForm.jsx
  16. 1 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  17. 1 1
      packages/app/src/components/PageAccessoriesModal.module.scss
  18. 14 0
      packages/app/src/components/PageComment.module.scss
  19. 1 4
      packages/app/src/components/PageComment.tsx
  20. 87 0
      packages/app/src/components/PageComment/Comment.module.scss
  21. 4 2
      packages/app/src/components/PageComment/Comment.tsx
  22. 1 0
      packages/app/src/components/PageComment/CommentControl.tsx
  23. 32 0
      packages/app/src/components/PageComment/CommentEditor.module.scss
  24. 4 2
      packages/app/src/components/PageComment/CommentEditor.tsx
  25. 10 0
      packages/app/src/components/PageComment/DeleteCommentModal.module.scss
  26. 3 1
      packages/app/src/components/PageComment/DeleteCommentModal.tsx
  27. 11 0
      packages/app/src/components/PageComment/ReplyComments.module.scss
  28. 4 2
      packages/app/src/components/PageComment/ReplyComments.tsx
  29. 34 0
      packages/app/src/components/PageComment/_comment-inheritance.scss
  30. 8 0
      packages/app/src/components/PageContentFooter.module.scss
  31. 2 1
      packages/app/src/components/PageContentFooter.tsx
  32. 1 9
      packages/app/src/components/PageEditor.tsx
  33. 3 1
      packages/app/src/components/PageEditor/Editor.tsx
  34. 14 12
      packages/app/src/components/PageList/PageListItemL.tsx
  35. 3 3
      packages/app/src/components/PagePathHierarchicalLink.tsx
  36. 6 1
      packages/app/src/components/PageTimeline.tsx
  37. 2 2
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  38. 5 3
      packages/app/src/components/SearchPage/SearchResultContent.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. 2 1
      packages/app/src/linter-checker/test.scss
  52. 17 7
      packages/app/src/pages/admin/[[...path]].page.tsx
  53. 5 6
      packages/app/src/pages/utils/objectid-transformer.ts
  54. 6 4
      packages/app/src/services/renderer/renderer.ts
  55. 5 43
      packages/app/src/styles/_comment.scss
  56. 2 121
      packages/app/src/styles/_comment_growi.scss
  57. 0 6
      packages/app/src/styles/_page-content-footer.scss
  58. 23 4
      packages/app/src/styles/_wiki.scss
  59. 1 1
      packages/codemirror-textlint/package.json
  60. 1 1
      packages/core/package.json
  61. 1 0
      packages/core/src/index.ts
  62. 27 0
      packages/core/src/test/util/objectid-utils.test.ts
  63. 21 0
      packages/core/src/utils/objectid-utils.ts
  64. 2 3
      packages/core/src/utils/page-path-utils.ts
  65. 1 1
      packages/plugin-attachment-refs/package.json
  66. 1 1
      packages/plugin-lsx/package.json
  67. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  68. 1 1
      packages/slack/package.json
  69. 2 2
      packages/slackbot-proxy/package.json
  70. 2 2
      packages/ui/package.json

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

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

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

+ 2 - 4
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';
@@ -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;

+ 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

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

@@ -1,13 +1,13 @@
 import React from 'react';
 
 import i18next from 'i18next';
-
 import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
 
-import { useCsrfToken } from '~/stores/context';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
+import { useCsrfToken } from '~/stores/context';
+
 class InstallerForm extends React.Component {
 
   constructor(props) {
@@ -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>

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

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

@@ -1,3 +1,17 @@
+.page-comment-styles :global {
+  .page-comments {
+    h4 {
+      margin-bottom: 1em;
+    }
+  }
+
+  // reply button
+  .btn-comment-reply {
+    margin-top: 0.5em;
+    border: none;
+  }
+}
+
 // TODO: Should Soft Coding see: https://github.com/weseek/growi/pull/6404
 .page-comment-comment-skelton :global {
   width: 300px;

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

@@ -19,9 +19,6 @@ import { CommentEditor } from './PageComment/CommentEditor';
 import { CommentEditorLazyRenderer } from './PageComment/CommentEditorLazyRenderer';
 import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
 import { ReplyComments } from './PageComment/ReplyComments';
-import { Skelton } from './Skelton';
-
-import styles from './PageComment.module.scss';
 
 type Props = {
   pageId?: Nullable<string>
@@ -195,7 +192,7 @@ export const PageComment: FC<Props> = memo((props:Props): JSX.Element => {
   return (
     <>
       {/* ToDO: Check the comment.html CSS */}
-      <div className="page-comments-row comment-list">
+      <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
         <div className="container-lg">
           <div className="page-comments">
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>

+ 87 - 0
packages/app/src/components/PageComment/Comment.module.scss

@@ -0,0 +1,87 @@
+@use '../../styles/bootstrap/init' as bs;
+@use './_comment-inheritance';
+
+.comment-styles :global {
+  .page-comment-writer {
+    @include bs.media-breakpoint-down(xs) {
+      height: 3.5em;
+    }
+  }
+
+  .page-comment {
+    position: relative;
+    padding-top: 70px;
+    margin-top: -70px;
+    pointer-events: none;
+
+    // user name
+    .page-comment-creator {
+      margin-top: -0.5em;
+      margin-bottom: 0.5em;
+      font-weight: bold;
+    }
+
+    // user icon
+    .picture {
+      @extend %picture;
+    }
+
+    // comment section
+    .page-comment-main {
+      @extend %comment-section;
+      @include bs.media-breakpoint-up(sm) {
+        margin-left: 4.5em;
+      }
+      @include bs.media-breakpoint-down(xs) {
+        &:before {
+          content: none;
+        }
+      }
+
+      pointer-events: auto;
+
+      // delete button
+      .page-comment-control {
+        position: absolute;
+        top: 0;
+        right: 0;
+        visibility: hidden;
+      }
+
+      &:hover > .page-comment-control {
+        visibility: visible;
+      }
+    }
+
+    // comment body
+    .page-comment-body {
+      margin-bottom: 0.5em;
+      word-wrap: break-word;
+    }
+
+    // older comments
+    &.page-comment-older {
+    }
+    // newer comments
+    &.page-comment-newer {
+      opacity: 0.7;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+
+    .page-comment-meta {
+      display: flex;
+      justify-content: flex-end;
+
+      font-size: 0.9em;
+      color: bs.$gray-400;
+    }
+
+    .page-comment-revision svg {
+      width: 16px;
+      height: 16px;
+    }
+  }
+}

+ 4 - 2
packages/app/src/components/PageComment/Comment.tsx

@@ -17,6 +17,8 @@ import Username from '../User/Username';
 import { CommentControl } from './CommentControl';
 import { CommentEditor } from './CommentEditor';
 
+import styles from './Comment.module.scss';
+
 type CommentProps = {
   comment: ICommentHasId,
   isReadOnly: boolean,
@@ -116,7 +118,7 @@ export const Comment = (props: CommentProps): JSX.Element => {
     : null;
 
   return (
-    <>
+    <div className={`${styles['comment-styles']}`}>
       {(isReEdit && !isReadOnly) ? (
         <CommentEditor
           rendererOptions={rendererOptions}
@@ -168,6 +170,6 @@ export const Comment = (props: CommentProps): JSX.Element => {
         </div>
       )
       }
-    </>
+    </div>
   );
 };

+ 1 - 0
packages/app/src/components/PageComment/CommentControl.tsx

@@ -9,6 +9,7 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
   const { onClickEditBtn, onClickDeleteBtn } = props;
 
   return (
+    // The page-comment-control class is imported from Comment.module.scss
     <div className="page-comment-control">
       <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
         <i className="ti ti-pencil"></i>

+ 32 - 0
packages/app/src/components/PageComment/CommentEditor.module.scss

@@ -0,0 +1,32 @@
+@use '~/styles/bootstrap/init' as bs;
+@use './_comment-inheritance';
+
+// display cheatsheet for comment form only
+.comment-editor-styles :global {
+  .comment-form {
+    position: relative;
+    margin-top: 1em;
+
+    // user icon
+    .picture {
+      @extend %picture;
+    }
+
+    // seciton
+    .comment-form-main {
+      @extend %comment-section;
+      margin-left: 4.5em;
+      @include bs.media-breakpoint-down(xs) {
+        margin-left: 3.5em;
+      }
+    }
+
+    // textarea
+    .comment-write {
+      margin-bottom: 0.5em;
+    }
+    .comment-form-preview {
+      padding-top: 0.5em;
+    }
+  }
+}

+ 4 - 2
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -19,13 +19,15 @@ import {
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useIsMobile } from '~/stores/ui';
 
-
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import { SlackNotification } from '../SlackNotification';
 
 import { CommentPreview } from './CommentPreview';
 
+import styles from './CommentEditor.module.scss';
+
+
 const navTabMapping = {
   comment_editor: {
     Icon: () => <i className="icon-settings" />,
@@ -328,7 +330,7 @@ export const CommentEditor = (props: PropsType): JSX.Element => {
   };
 
   return (
-    <div className="form page-comment-form">
+    <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
       <div className="comment-form">
         <div className="comment-form-user">
           <UserPicture user={currentUser} noLink noTooltip />

+ 10 - 0
packages/app/src/components/PageComment/DeleteCommentModal.module.scss

@@ -0,0 +1,10 @@
+// modal
+.page-comment-delete-modal :global {
+  .modal-content .modal-body {
+    .comment-body {
+      max-height: 13em;
+      // scrollable
+      overflow-y: auto;
+    }
+  }
+}

+ 3 - 1
packages/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -9,6 +9,8 @@ import {
 import { ICommentHasId } from '../../interfaces/comment';
 import Username from '../User/Username';
 
+import styles from './DeleteCommentModal.module.scss';
+
 
 type DeleteCommentModalProps = {
   isShown: boolean,
@@ -39,7 +41,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
   const commentBodyElement = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
 
   return (
-    <Modal isOpen={isShown} toggle={cancel} className="page-comment-delete-modal">
+    <Modal isOpen={isShown} toggle={cancel} className={`${styles['page-comment-delete-modal']}`}>
       <ModalHeader tag="h4" toggle={cancel} className="bg-danger text-light">
         <span>
           <i className="icon-fw icon-fire"></i>

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

@@ -0,0 +1,11 @@
+/*
+* reply
+*/
+.page-comment-reply :global {
+  margin-top: 1em;
+}
+
+// remove margin after hidden replies
+.page-comments-hidden-replies + .page-comment-reply :global {
+  margin-top: 0;
+}

+ 4 - 2
packages/app/src/components/PageComment/ReplyComments.tsx

@@ -10,6 +10,8 @@ import { useIsAllReplyShown } from '../../stores/context';
 
 import { Comment } from './Comment';
 
+import styles from './ReplyComments.module.scss';
+
 type ReplycommentsProps = {
   isReadOnly: boolean,
   replyList: ICommentHasIdList,
@@ -34,7 +36,7 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 
   const renderReply = (reply: ICommentHasId) => {
     return (
-      <div key={reply._id} className="page-comment-reply ml-4 ml-sm-5 mr-3">
+      <div key={reply._id} className={`${styles['page-comment-reply']} ml-4 ml-sm-5 mr-3`}>
         <Comment
           rendererOptions={rendererOptions}
           deleteBtnClicked={deleteBtnClicked}
@@ -77,7 +79,7 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
   return (
     <>
       {areThereHiddenReplies && (
-        <div className="page-comments-hidden-replies">
+        <div className={`${styles['page-comments-hidden-replies']}`}>
           <Collapse isOpen={isOlderRepliesShown}>
             <div>{hiddenElements}</div>
           </Collapse>

+ 34 - 0
packages/app/src/components/PageComment/_comment-inheritance.scss

@@ -0,0 +1,34 @@
+@use '../../styles/bootstrap/init' as bs;
+
+%comment-section {
+  position: relative;
+  padding: 1em;
+
+  // speech balloon
+  &:before {
+    position: absolute;
+    top: 1.5em;
+    left: -1em;
+    display: block;
+    width: 0;
+    content: '';
+    border: 1em solid transparent;
+    border-left-width: 0;
+
+    @include bs.media-breakpoint-down(xs) {
+      top: 1em;
+    }
+  }
+}
+
+%picture {
+  float: left;
+  width: 3em;
+  height: 3em;
+  margin-top: 0.8em;
+
+  @include bs.media-breakpoint-down(xs) {
+    width: 2em;
+    height: 2em;
+  }
+}

+ 8 - 0
packages/app/src/components/PageContentFooter.module.scss

@@ -1,3 +1,11 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.page-content-footer :global {
+  border-top: solid 1px transparent;
+  .page-meta {
+    font-size: 0.95em;
+  }
+}
 // TODO: Should Soft Coding see: https://github.com/weseek/growi/pull/6404
 .page-content-footer-skelton :global {
   width: 300px;

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

@@ -20,7 +20,8 @@ export const PageContentFooter = memo((): JSX.Element => {
   }
 
   return (
-    <div className="page-content-footer py-4 d-edit-none d-print-none">
+    // TODO: page-content-footer, scss module import and global import.
+    <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
       <div className="grw-container-convertible">
         <div className="page-meta">
           <AuthorInfo user={page.creator} date={page.createdAt} mode="create" locate="footer" />

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

+ 5 - 3
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -81,7 +81,8 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   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 });
+  // TODO: Commentout for eslint error
+  // const PageContentFooter = dynamic(() => import('../PageContentFooter'), { ssr: false });
 
   const scrollElementRef = useRef(null);
 
@@ -217,12 +218,13 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           highlightKeywords={highlightKeywords}
         />
         <PageComment pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
-        <PageContentFooter
+        {/* TODO: Commentout for eslint error */}
+        {/* <PageContentFooter
           // createdAt={new Date(pageWithMeta.data.createdAt)}
           // updatedAt={new Date(pageWithMeta.data.updatedAt)}
           // creator={pageWithMeta.data.creator}
           // revisionAuthor={pageWithMeta.data.lastUpdateUser}
-        />
+        /> */}
       </div>
     </div>
   );

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

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

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

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

+ 6 - 4
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';
@@ -223,10 +223,12 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
       slug,
       raw,
       [sanitize, {
-        ...defaultSchema,
+        ...sanitizeDefaultSchema,
         attributes: {
-          ...defaultSchema.attributes,
-          '*': ['className', 'class'],
+          ...sanitizeDefaultSchema.attributes,
+          '*': sanitizeDefaultSchema.attributes != null
+            ? sanitizeDefaultSchema.attributes['*'].concat('class', 'className')
+            : ['class', 'className'],
         },
       }],
       [addClass, {

+ 5 - 43
packages/app/src/styles/_comment.scss

@@ -9,57 +9,19 @@
   }
 }
 
+
 .page-comments {
+  // TODO: Never use .page-comments-list-toggle-older class.
   .page-comments-list-toggle-older {
     display: inline-block;
     font-size: 0.9em;
   }
+  // TODO: "pointer-events: none;" moved to "Comment.module.scss" now.
+  // .page-comment was defined in _comment.scss and _comment_growi.scss
+  // Required if .page-comment is not under .growi but under .page-comments, or under .growi but not under .page-comments
   .page-comment {
     padding-top: 50px;
     margin-top: -50px;
     pointer-events: none;
   }
-
-  .page-comment {
-    // older comments
-    &.page-comment-older {
-    }
-    // newer comments
-    &.page-comment-newer {
-      opacity: 0.7;
-
-      &:hover {
-        opacity: 1;
-      }
-    }
-
-    .page-comment-meta {
-      display: flex;
-      justify-content: flex-end;
-
-      font-size: 0.9em;
-      color: $gray-400;
-    }
-
-    .page-comment-revision svg {
-      width: 16px;
-      height: 16px;
-    }
-  }
-
-  .page-comment-main {
-    pointer-events: auto;
-
-    // delete button
-    .page-comment-control {
-      position: absolute;
-      top: 0;
-      right: 0;
-      visibility: hidden;
-    }
-
-    &:hover > .page-comment-control {
-      visibility: visible;
-    }
-  }
 }

+ 2 - 121
packages/app/src/styles/_comment_growi.scss

@@ -1,129 +1,13 @@
 .growi {
-  %comment-section {
-    position: relative;
-    padding: 1em;
-
-    // speech balloon
-    &:before {
-      position: absolute;
-      top: 1.5em;
-      left: -1em;
-      display: block;
-      width: 0;
-      content: '';
-      border: 1em solid transparent;
-      border-left-width: 0;
-
-      @include media-breakpoint-down(xs) {
-        top: 1em;
-      }
-    }
-  }
-
-  %picture {
-    float: left;
-    width: 3em;
-    height: 3em;
-    margin-top: 0.8em;
-
-    @include media-breakpoint-down(xs) {
-      width: 2em;
-      height: 2em;
-    }
-  }
-
-  .page-comments {
-    h4 {
-      margin-bottom: 1em;
-    }
-  }
-
-  .page-comment-writer {
-    @include media-breakpoint-down(xs) {
-      height: 3.5em;
-    }
-  }
-
-  .page-comment {
-    position: relative;
-    padding-top: 70px;
-    margin-top: -70px;
-
-    // ユーザー名
-    .page-comment-creator {
-      margin-top: -0.5em;
-      margin-bottom: 0.5em;
-      font-weight: bold;
-    }
-
-    // ユーザーアイコン
-    .picture {
-      @extend %picture;
-    }
-
-    // コメントセクション
-    .page-comment-main {
-      @extend %comment-section;
-      @include media-breakpoint-up(sm) {
-        margin-left: 4.5em;
-      }
-      @include media-breakpoint-down(xs) {
-        &:before {
-          content: none;
-        }
-      }
-    }
-
-    // コメント本文
-    .page-comment-body {
-      margin-bottom: 0.5em;
-      word-wrap: break-word;
-    }
-  }
-
-  /*
-   * reply
-   */
-  .page-comment-reply {
-    margin-top: 1em;
-  }
-  // remove margin after hidden replies
-  .page-comments-hidden-replies + .page-comment-reply {
-    margin-top: 0;
-  }
-  // reply button
-  .btn.btn-comment-reply {
-    margin-top: 0.5em;
-    border: none;
-  }
-
   // display cheatsheet for comment form only
   .comment-form {
+    // TODO: Never use .editor-cheatsheet class.
     .editor-cheatsheet {
       display: none;
     }
 
-    position: relative;
-    margin-top: 1em;
-
-    // user icon
-    .picture {
-      @extend %picture;
-    }
-
-    // seciton
-    .comment-form-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      @include media-breakpoint-down(xs) {
-        margin-left: 3.5em;
-      }
-    }
-
     // textarea
-    .comment-write {
-      margin-bottom: 0.5em;
-    }
+    // TODO: Never use .comment-form-comment class.
     .comment-form-comment {
       height: 80px;
       &:focus,
@@ -132,8 +16,5 @@
         transition: height 0.2s ease-out;
       }
     }
-    .comment-form-preview {
-      padding-top: 0.5em;
-    }
   }
 }

+ 0 - 6
packages/app/src/styles/_page-content-footer.scss

@@ -1,6 +0,0 @@
-.page-content-footer {
-  border-top: solid 1px transparent;
-  .page-meta {
-    font-size: 0.95em;
-  }
-}

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