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

Merge branch 'dev/4.0.x' into feat/history

# Conflicts:
#	src/client/js/components/Navbar/GrowiSubNavigation.jsx
#	src/client/styles/scss/_subnav.scss
#	src/lib/components/PagePathHierarchicalLink.jsx
Yuki Takei 5 лет назад
Родитель
Сommit
1b6438d5dc
74 измененных файлов с 1205 добавлено и 1300 удалено
  1. 1 0
      babel.config.js
  2. 4 3
      config/webpack.common.js
  3. 1 0
      package.json
  4. 1 5
      resource/locales/en-US/admin/admin.json
  5. 1 1
      resource/locales/en-US/translation.json
  6. 1 5
      resource/locales/ja/admin/admin.json
  7. 1 1
      resource/locales/ja/translation.json
  8. 2 4
      src/client/js/app.jsx
  9. 5 5
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  10. 51 30
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  11. 27 5
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  12. 89 43
      src/client/js/components/Page/CopyDropdown.jsx
  13. 0 55
      src/client/js/components/Page/RevisionPath.jsx
  14. 47 0
      src/client/js/components/Page/RevisionPathControls.jsx
  15. 50 0
      src/client/js/components/PageEditor/PagePathNavForEditor.jsx
  16. 7 15
      src/client/js/components/PageList/PageListMeta.jsx
  17. 8 31
      src/client/js/legacy/crowi.js
  18. 0 5
      src/client/styles/scss/_comment_crowi.scss
  19. 0 38
      src/client/styles/scss/_layout_crowi.scss
  20. 0 201
      src/client/styles/scss/_layout_crowi_sidebar.scss
  21. 28 25
      src/client/styles/scss/_on-edit.scss
  22. 0 4
      src/client/styles/scss/_page.scss
  23. 2 1
      src/client/styles/scss/_staff_credit.scss
  24. 7 0
      src/client/styles/scss/_subnav.scss
  25. 19 0
      src/client/styles/scss/molecules/copy-dropdown.scss
  26. 3 3
      src/client/styles/scss/style-app.scss
  27. 1 0
      src/client/styles/scss/theme/_apply-colors.scss
  28. 189 6
      src/client/styles/scss/theme/mono-blue.scss
  29. 174 8
      src/client/styles/scss/theme/nature.scss
  30. 170 6
      src/client/styles/scss/theme/spring.scss
  31. 167 6
      src/client/styles/scss/theme/wood.scss
  32. 4 4
      src/lib/components/PagePathHierarchicalLink.jsx
  33. 2 0
      src/lib/models/devided-page-path.js
  34. 6 0
      src/lib/models/linked-page-path.js
  35. 9 0
      src/lib/util/path-utils.js
  36. 38 0
      src/migrations/20200512005851-remove-behavior-type.js
  37. 1 4
      src/server/models/config.js
  38. 4 11
      src/server/models/page.js
  39. 0 1
      src/server/routes/apiv3/customize-setting.js
  40. 29 107
      src/server/routes/page.js
  41. 0 7
      src/server/views/customlayout-selector/forbidden.html
  42. 0 7
      src/server/views/customlayout-selector/not_creatable.html
  43. 0 7
      src/server/views/customlayout-selector/not_found.html
  44. 0 7
      src/server/views/customlayout-selector/page.html
  45. 0 7
      src/server/views/customlayout-selector/page_list.html
  46. 0 7
      src/server/views/customlayout-selector/user_page.html
  47. 0 58
      src/server/views/layout-crowi/base/layout.html
  48. 0 40
      src/server/views/layout-crowi/forbidden.html
  49. 0 40
      src/server/views/layout-crowi/not_creatable.html
  50. 0 40
      src/server/views/layout-crowi/not_found.html
  51. 0 69
      src/server/views/layout-crowi/page.html
  52. 0 83
      src/server/views/layout-crowi/page_list.html
  53. 0 19
      src/server/views/layout-crowi/user_page.html
  54. 0 23
      src/server/views/layout-crowi/widget/page_side_content.html
  55. 0 43
      src/server/views/layout-crowi/widget/page_side_header.html
  56. 0 2
      src/server/views/layout-growi/page.html
  57. 1 3
      src/server/views/layout-growi/page_list.html
  58. 0 2
      src/server/views/layout-growi/user_page.html
  59. 0 6
      src/server/views/layout-growi/widget/header.html
  60. 1 0
      src/server/views/layout-kibela/not_found.html
  61. 0 2
      src/server/views/layout-kibela/page.html
  62. 1 3
      src/server/views/layout-kibela/page_list.html
  63. 0 2
      src/server/views/layout-kibela/user_page.html
  64. 0 5
      src/server/views/layout-kibela/widget/header.html
  65. 2 2
      src/server/views/modal/create_page.html
  66. 0 50
      src/server/views/modal/unportalize.html
  67. 0 81
      src/server/views/modal/what_is_portal.html
  68. 0 4
      src/server/views/widget/create_portal.html
  69. 2 0
      src/server/views/widget/not_found_tabs.html
  70. 0 3
      src/server/views/widget/page_content.html
  71. 2 2
      src/server/views/widget/page_list.html
  72. 3 13
      src/server/views/widget/page_tabs.html
  73. 29 30
      src/server/views/widget/page_tabs_kibela.html
  74. 15 0
      yarn.lock

+ 1 - 0
babel.config.js

@@ -26,6 +26,7 @@ module.exports = function(api) {
         },
       },
     ],
+    '@babel/plugin-proposal-optional-chaining',
     [
       '@babel/plugin-proposal-class-properties', { loose: true },
     ],

+ 4 - 3
config/webpack.common.js

@@ -34,16 +34,17 @@ module.exports = (options) => {
       'styles/style-presentation':    './src/client/styles/scss/style-presentation.scss',
       // themes
       'styles/theme-default':         './src/client/styles/scss/theme/default.scss',
-      // 'styles/theme-nature':          './src/client/styles/scss/theme/nature.scss',
-      // 'styles/theme-mono-blue':       './src/client/styles/scss/theme/mono-blue.scss',
+      'styles/theme-nature':          './src/client/styles/scss/theme/nature.scss',
+      'styles/theme-mono-blue':       './src/client/styles/scss/theme/mono-blue.scss',
       // 'styles/theme-future':          './src/client/styles/scss/theme/future.scss',
       // 'styles/theme-blue-night':      './src/client/styles/scss/theme/blue-night.scss',
       'styles/theme-kibela':          './src/client/styles/scss/theme/kibela.scss',
       'styles/theme-halloween':       './src/client/styles/scss/theme/halloween.scss',
-      // 'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
+      'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
       // 'styles/theme-christmas':          './src/client/styles/scss/theme/christmas.scss',
       // 'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
+      'styles/theme-spring':         './src/client/styles/scss/theme/spring.scss',
       // styles for external services
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
     }, options.entry || {}), // Merge with env dependent settings

+ 1 - 0
package.json

@@ -158,6 +158,7 @@
     "@atlaskit/navigation-next": "^8.0.2",
     "@babel/core": "^7.4.5",
     "@babel/plugin-proposal-class-properties": "^7.8.3",
+    "@babel/plugin-proposal-optional-chaining": "^7.9.0",
     "@babel/polyfill": "^7.4.4",
     "@babel/preset-env": "^7.4.5",
     "@babel/preset-react": "^7.0.0",

+ 1 - 5
resource/locales/en-US/admin/admin.json

@@ -97,11 +97,7 @@
       "kibela_title": "Easy Viewing Structure",
       "kibela_text1": "Center aligned contents",
       "kibela_text2": "Show and post comments at the bottom of the page",
-      "kibela_text3": "Affix Table-of-contents",
-      "crowi_title": "Separated Functions",
-      "crowi_text1": "Collapsible Sidebar",
-      "crowi_text2": "Show and post comments in Sidebar",
-      "crowi_text3": "Collapsible Table-of-contents"
+      "kibela_text3": "Affix Table-of-contents"
     },
     "function": "Function",
     "function_desc": "You can choose Valid/Invalid of the function",

+ 1 - 1
resource/locales/en-US/translation.json

@@ -55,7 +55,6 @@
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Create/Edit Template": "Create/Edit Template Page",
-  "Unportalize": "Unportalize",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "No diff": "No diff",
@@ -200,6 +199,7 @@
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
+    "Page URL": "Page URL",
     "Parmanent link": "Parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"

+ 1 - 5
resource/locales/ja/admin/admin.json

@@ -97,11 +97,7 @@
       "kibela_title": "閲覧重視の構造",
       "kibela_text1": "コンテンツが中心に表示されます。",
       "kibela_text2": "コメントはページの下部に表示されます。",
-      "kibela_text3": "ページ情報は下部に表示されます。",
-      "crowi_title": "ビュー・コントロールの分離",
-      "crowi_text1": "サイドバーを開くと情報が表示されます。",
-      "crowi_text2": "コメントはサイドバーに表示されます。",
-      "crowi_text3": "ページ情報はサイドバーに表示されます。"
+      "kibela_text3": "ページ情報は下部に表示されます。"
     },
     "function": "機能",
     "function_desc": "機能の有効/無効を選択できます。",

+ 1 - 1
resource/locales/ja/translation.json

@@ -55,7 +55,6 @@
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
   "Create/Edit Template": "テンプレートページの作成/編集",
-  "Unportalize": "ポータル解除",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "No diff": "差分なし",
@@ -199,6 +198,7 @@
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
+    "Page URL": "ページURL",
     "Parmanent link": "パーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Markdown link": "マークダウン形式のリンク"

+ 2 - 4
src/client/js/app.jsx

@@ -8,6 +8,7 @@ import loggerFactory from '@alias/logger';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
+import PagePathNavForEditor from './components/PageEditor/PagePathNavForEditor';
 // eslint-disable-next-line import/no-duplicates
 import OptionsSelector from './components/PageEditor/OptionsSelector';
 // eslint-disable-next-line import/no-duplicates
@@ -21,8 +22,6 @@ import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
-import RevisionPath from './components/Page/RevisionPath';
-import TagLabels from './components/Page/TagLabels';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyDraftList from './components/MyDraftList/MyDraftList';
@@ -71,6 +70,7 @@ Object.assign(componentMappings, {
   'create-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} addTrailingSlash />,
 
   'page-editor': <PageEditor />,
+  'page-editor-path-nav': <PagePathNavForEditor />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls />,
@@ -101,8 +101,6 @@ if (pageContainer.state.path != null) {
   Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     'page': <Page />,
-    'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
-    'tag-label': <TagLabels />,
     'grw-subnav': <GrowiSubNavigation />,
     'grw-subnav-for-user-page': <GrowiSubNavigationForUserPage />,
   });

+ 5 - 5
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -15,7 +15,7 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 
-class CustomizeBehaviorSetting extends React.Component {
+class CustomizeFunctionSetting extends React.Component {
 
   constructor(props) {
     super(props);
@@ -173,14 +173,14 @@ class CustomizeBehaviorSetting extends React.Component {
 
 }
 
-const CustomizeBehaviorSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeBehaviorSetting, props, [AppContainer, AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = (props) => {
+  return createSubscribedElement(CustomizeFunctionSetting, props, [AppContainer, AdminCustomizeContainer]);
 };
 
-CustomizeBehaviorSetting.propTypes = {
+CustomizeFunctionSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 
-export default withTranslation()(CustomizeBehaviorSettingWrapper);
+export default withTranslation()(CustomizeFunctionSettingWrapper);

+ 51 - 30
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -12,7 +12,7 @@ import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLi
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 
-import RevisionPath from '../Page/RevisionPath';
+import RevisionPathControls from '../Page/RevisionPathControls';
 import PageContainer from '../../services/PageContainer';
 import TagLabels from '../Page/TagLabels';
 import LikeButton from '../LikeButton';
@@ -21,6 +21,42 @@ import BookmarkButton from '../BookmarkButton';
 import PageCreator from './PageCreator';
 import RevisionAuthor from './RevisionAuthor';
 
+// eslint-disable-next-line react/prop-types
+const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
+
+  const dPagePath = new DevidedPagePath(pagePath, false, true);
+
+  let formerLink;
+  let latterLink;
+
+  // when the path is root or first level
+  if (dPagePath.isRoot || dPagePath.isFormerRoot) {
+    const linkedPagePath = new LinkedPagePath(pagePath);
+    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
+  }
+  // when the path is second level or deeper
+  else {
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+    formerLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />;
+    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} />;
+  }
+
+  return (
+    <div className="grw-page-path-nav">
+      {formerLink}
+      <span className="d-flex align-items-center flex-wrap">
+        <h1 className="m-0">{latterLink}</h1>
+        <RevisionPathControls
+          pageId={pageId}
+          pagePath={pagePath}
+          isPageForbidden={isPageForbidden}
+        />
+      </span>
+    </div>
+  );
+};
+
 const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page') === 'true';
   const { appContainer, pageContainer } = props;
@@ -31,27 +67,11 @@ const GrowiSubNavigation = (props) => {
   const isPageNotFound = pageId == null;
   const isPageInTrash = isTrashPage(path);
 
-  const dPagePath = new DevidedPagePath(pageContainer.state.path, false, true);
-  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-  const FormerLink = () => (
-    <div className="grw-page-path-text-muted-container">
-      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
-    </div>
-  );
-
   // Display only the RevisionPath
-  if (isPageNotFound || isPageForbidden || isPageInTrash) {
+  if (isPageNotFound || isPageForbidden) {
     return (
       <div className="px-3 py-3 grw-subnavbar">
-        { !dPagePath.isRoot && <FormerLink /> }
-        <h1 className="m-0">
-          <RevisionPath
-            pageId={pageId}
-            pagePath={pageContainer.state.path}
-            isPageForbidden={isPageForbidden}
-            isPageInTrash={isPageInTrash}
-          />
-        </h1>
+        <PagePathNav pageId={pageId} pagePath={path} isPageForbidden={isPageForbidden} />
       </div>
     );
   }
@@ -73,23 +93,24 @@ const GrowiSubNavigation = (props) => {
 
       {/* Page Path */}
       <div>
-        { !dPagePath.isRoot && <FormerLink /> }
-        <h1 className="m-0">
-          <RevisionPath pageId={pageId} pagePath={pageContainer.state.path} />
-        </h1>
+        <PagePathNav pageId={pageId} pagePath={path} isPageForbidden={isPageForbidden} />
         { !isPageNotFound && !isPageForbidden && (
           <TagLabels />
         ) }
       </div>
 
       <div className="d-flex align-items-center">
-        {/* Header Button */}
-        <div className="mr-2">
-          <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
-        </div>
-        <div>
-          <BookmarkButton pageId={pageId} crowi={appContainer} />
-        </div>
+        { !isPageInTrash && (
+          /* Header Button */
+          <div className="mr-2">
+            <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
+          </div>
+        ) }
+        { !isPageInTrash && (
+          <div>
+            <BookmarkButton pageId={pageId} crowi={appContainer} />
+          </div>
+        ) }
 
         {/* Page Authors */}
         <ul className="authors text-nowrap d-none d-lg-block d-edit-none">

+ 27 - 5
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -3,17 +3,41 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
+import LinkedPagePath from '@commons/models/linked-page-path';
+import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLink';
+
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
-import RevisionPath from '../Page/RevisionPath';
 import PageContainer from '../../services/PageContainer';
+
+import RevisionPathControls from '../Page/RevisionPathControls';
 import BookmarkButton from '../BookmarkButton';
 import UserPicture from '../User/UserPicture';
 
+// eslint-disable-next-line react/prop-types
+const PagePathNav = ({ pageId, pagePath }) => {
+  const linkedPagePath = new LinkedPagePath(pagePath);
+  const latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
+
+  return (
+    <div className="grw-page-path-nav">
+      <span className="d-flex align-items-center flex-wrap">
+        <h4 className="grw-user-page-path">{latterLink}</h4>
+        <RevisionPathControls
+          pageId={pageId}
+          pagePath={pagePath}
+        />
+      </span>
+    </div>
+  );
+};
+
 const GrowiSubNavigationForUserPage = (props) => {
   const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
   const { appContainer, pageContainer } = props;
-  const { pageId, isHeaderSticky, isSubnavCompact } = pageContainer.state;
+  const {
+    pageId, path, isHeaderSticky, isSubnavCompact,
+  } = pageContainer.state;
 
   const additionalClassNames = ['grw-subnavbar', 'grw-subnavbar-user-page'];
   const layoutType = appContainer.getConfig().layoutType;
@@ -32,9 +56,7 @@ const GrowiSubNavigationForUserPage = (props) => {
 
   return (
     <div className={`px-3 ${additionalClassNames.join(' ')}`}>
-      <h4 className="grw-user-page-path">
-        <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
-      </h4>
+      <PagePathNav pageId={pageId} pagePath={path} />
 
       <div className="d-flex align-items-center justify-content-between">
 

+ 89 - 43
src/client/js/components/Page/CopyDropdown.jsx

@@ -15,9 +15,6 @@ class CopyDropdown extends React.Component {
   constructor(props) {
     super(props);
 
-    // retrieve xss library from window
-    this.xss = window.xss;
-
     this.state = {
       dropdownOpen: false,
       tooltipOpen: false,
@@ -25,7 +22,10 @@ class CopyDropdown extends React.Component {
 
     this.toggle = this.toggle.bind(this);
     this.showToolTip = this.showToolTip.bind(this);
-    this.generatePageUrl = this.generatePageUrl.bind(this);
+    this.generatePagePathWithParams = this.generatePagePathWithParams.bind(this);
+    this.generatePagePathUrl = this.generatePagePathUrl.bind(this);
+    this.generatePermalink = this.generatePermalink.bind(this);
+    this.generateMarkdownLink = this.generateMarkdownLink.bind(this);
   }
 
   toggle() {
@@ -39,25 +39,65 @@ class CopyDropdown extends React.Component {
     }, 1000);
   }
 
-  generatePageUrl() {
-    return (this.props.pageId == null)
-      ? decodeURIComponent(window.location.pathname + window.location.search)
-      : `${window.location.origin}/${this.props.pageId}`;
+  generatePagePathWithParams() {
+    const { pagePath } = this.props;
+    const {
+      search, hash,
+    } = window.location;
+
+    return decodeURI(`${pagePath}${search}${hash}`);
+  }
+
+  generatePagePathUrl() {
+    const { origin } = window.location;
+    return `${origin}${this.generatePagePathWithParams()}`;
+  }
+
+  generatePermalink() {
+    const { pageId } = this.props;
+    const { location } = window;
+
+    if (pageId == null) {
+      return null;
+    }
+
+    const {
+      origin, search, hash,
+    } = location;
+    return decodeURI(`${origin}/${pageId}${search}${hash}`);
   }
 
   generateMarkdownLink() {
-    return;
+    const { pagePath } = this.props;
+    const {
+      search, hash,
+    } = window.location;
+
+    const label = `${pagePath}${search}${hash}`;
+    const permalink = this.generatePermalink();
+
+    return decodeURI(`[${label}](${permalink})`);
   }
 
+  DropdownItemContents = ({ title, contents }) => (
+    <>
+      <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
+      <div className="card well mb-1 p-2">{contents}</div>
+    </>
+  );
+
   render() {
-    const { t } = this.props;
+    const { t, pageId } = this.props;
+
+    const pagePathWithParams = this.generatePagePathWithParams();
+    const pagePathUrl = this.generatePagePathUrl();
+    const permalink = this.generatePermalink();
 
-    const safePagePath = this.xss.process(this.props.pagePath);
-    const url = this.generatePageUrl();
+    const { DropdownItemContents } = this;
 
     return (
       <>
-        <Dropdown id="copyPagePathDropdown" isOpen={this.state.dropdownOpen} toggle={this.toggle}>
+        <Dropdown id="copyPagePathDropdown" className="grw-copy-dropdown" isOpen={this.state.dropdownOpen} toggle={this.toggle}>
 
           <DropdownToggle
             caret
@@ -68,49 +108,55 @@ class CopyDropdown extends React.Component {
           </DropdownToggle>
 
           <DropdownMenu>
-            <h5 className="ml-3 my-0 text-muted">{ t('copy_to_clipboard.Copy to clipboard') }</h5>
-            <DropdownItem divider></DropdownItem>
+            <DropdownItem header className="px-3">{ t('copy_to_clipboard.Copy to clipboard') }</DropdownItem>
+
+            <DropdownItem divider className="my-0"></DropdownItem>
 
             {/* Page path */}
-            <CopyToClipboard text={this.props.pagePath} onCopy={this.showToolTip}>
-              <DropdownItem tag="a">
-                <div className="d-inline-flex flex-column">
-                  <h6 className="mt-1 mb-2"><strong>{ t('copy_to_clipboard.Page path') }</strong></h6>
-                  <span className="small">{safePagePath}</span>
-                </div>
+            <CopyToClipboard text={pagePathWithParams} onCopy={this.showToolTip}>
+              <DropdownItem className="px-3">
+                <DropdownItemContents title={t('copy_to_clipboard.Page path')} contents={pagePathWithParams} />
+              </DropdownItem>
+            </CopyToClipboard>
+
+            <DropdownItem divider className="my-0"></DropdownItem>
+
+            {/* Page path URL */}
+            <CopyToClipboard text={pagePathUrl} onCopy={this.showToolTip}>
+              <DropdownItem className="px-3">
+                <DropdownItemContents title={t('copy_to_clipboard.Page URL')} contents={pagePathUrl} />
               </DropdownItem>
             </CopyToClipboard>
+
+            <DropdownItem divider className="my-0"></DropdownItem>
+
             {/* Parmanent Link */}
-            { this.props.pageId && (
-              <CopyToClipboard text={url} onCopy={this.showToolTip}>
-                <DropdownItem tag="a">
-                  <div className="d-inline-flex flex-column">
-                    <h6 className="mt-1 mb-2"><strong>{ t('copy_to_clipboard.Parmanent link') }</strong></h6>
-                    <span className="small">{url}</span>
-                  </div>
+            { pageId && (
+              <CopyToClipboard text={permalink} onCopy={this.showToolTip}>
+                <DropdownItem className="px-3">
+                  <DropdownItemContents title={t('copy_to_clipboard.Parmanent link')} contents={permalink} />
                 </DropdownItem>
               </CopyToClipboard>
             )}
+
+            <DropdownItem divider className="my-0"></DropdownItem>
+
             {/* Page path + Parmanent Link */}
-            { this.props.pageId && (
-              <CopyToClipboard text={`${this.props.pagePath}\n${url}`} onCopy={this.showToolTip}>
-                <DropdownItem tag="a">
-                  <div className="d-inline-flex flex-column">
-                    <h6 className="mt-1 mb-2"><strong>{ t('copy_to_clipboard.Page path and parmanent link') }</strong></h6>
-                    <span className="small mb-3">{safePagePath}</span>
-                    <span className="small">{url}</span>
-                  </div>
+            { pageId && (
+              <CopyToClipboard text={`${pagePathWithParams}\n${permalink}`} onCopy={this.showToolTip}>
+                <DropdownItem className="px-3">
+                  <DropdownItemContents title={t('copy_to_clipboard.Page path and parmanent link')} contents={<>{pagePathWithParams}<br />{permalink}</>} />
                 </DropdownItem>
               </CopyToClipboard>
             )}
+
+            <DropdownItem divider className="my-0"></DropdownItem>
+
             {/* Markdown Link */}
-            { this.props.pageId && (
-              <CopyToClipboard text={`[${this.props.pagePath}](${url})`} onCopy={this.showToolTip}>
-                <DropdownItem tag="a">
-                  <div className="d-inline-flex flex-column">
-                    <h6 className="mt-1 mb-2"><strong>{ t('copy_to_clipboard.Markdown link') }</strong></h6>
-                    <span className="small">{`[${safePagePath}](${url})`}</span>
-                  </div>
+            { pageId && (
+              <CopyToClipboard text={this.generateMarkdownLink()} onCopy={this.showToolTip}>
+                <DropdownItem className="px-3 text-wrap">
+                  <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={this.generateMarkdownLink()} isContentsWrap />
                 </DropdownItem>
               </CopyToClipboard>
             )}

+ 0 - 55
src/client/js/components/Page/RevisionPath.jsx

@@ -1,55 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import DevidedPagePath from '@commons/models/devided-page-path';
-import LinkedPagePath from '@commons/models/linked-page-path';
-import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLink';
-
-import CopyDropdown from './CopyDropdown';
-
-const RevisionPath = (props) => {
-  // define styles
-  const buttonStyle = {
-    marginLeft: '0.5em',
-    padding: '0 2px',
-  };
-
-  const {
-    pageId, isPageInTrash, isPageForbidden,
-  } = props;
-
-  const dPagePath = new DevidedPagePath(props.pagePath, false, true);
-  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-
-  return (
-    <>
-      <span className="d-flex align-items-center flex-wrap">
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-        <CopyDropdown pagePath={props.pagePath} pageId={pageId} buttonStyle={buttonStyle} />
-        { !isPageInTrash && !isPageForbidden && (
-          <a href="#edit" className="d-block d-edit-none text-muted btn btn-secondary bg-transparent btn-edit border-0" style={buttonStyle}>
-            <i className="icon-note" />
-          </a>
-        ) }
-      </span>
-    </>
-  );
-};
-
-RevisionPath.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  pagePath: PropTypes.string.isRequired,
-  pageId: PropTypes.string,
-  isPageForbidden: PropTypes.bool,
-  isPageInTrash: PropTypes.bool,
-};
-
-RevisionPath.defaultProps = {
-  isPageForbidden: false,
-  isPageInTrash: false,
-};
-
-export default withTranslation()(RevisionPath);

+ 47 - 0
src/client/js/components/Page/RevisionPathControls.jsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { isTrashPage } from '@commons/util/path-utils';
+
+import CopyDropdown from './CopyDropdown';
+
+const RevisionPathControls = (props) => {
+  // define styles
+  const buttonStyle = {
+    marginLeft: '0.5em',
+    padding: '0 2px',
+  };
+
+  const {
+    pagePath, pageId, isPageForbidden,
+  } = props;
+
+  const isPageInTrash = isTrashPage(pagePath);
+
+  return (
+    <>
+      <CopyDropdown pagePath={pagePath} pageId={pageId} buttonStyle={buttonStyle} />
+      { !isPageInTrash && !isPageForbidden && (
+        <a href="#edit" className="d-edit-none text-muted btn btn-secondary bg-transparent btn-edit border-0" style={buttonStyle}>
+          <i className="icon-note" />
+        </a>
+      ) }
+    </>
+  );
+};
+
+RevisionPathControls.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  pagePath: PropTypes.string.isRequired,
+  pageId: PropTypes.string,
+  isPageForbidden: PropTypes.bool,
+};
+
+RevisionPathControls.defaultProps = {
+  isPageForbidden: false,
+};
+
+export default withTranslation()(RevisionPathControls);

+ 50 - 0
src/client/js/components/PageEditor/PagePathNavForEditor.jsx

@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import LinkedPagePath from '@commons/models/linked-page-path';
+import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLink';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+
+import RevisionPathControls from '../Page/RevisionPathControls';
+import TagLabels from '../Page/TagLabels';
+
+const PagePathNavForEditor = (props) => {
+  const { pageId, path } = props.pageContainer.state;
+
+  const linkedPagePath = new LinkedPagePath(path);
+  const pagePathHierarchicalLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
+
+  return (
+    <div className="grw-page-path-nav-for-edit mt-1">
+      <span className="d-flex align-items-center flex-wrap">
+        <h3 className="mb-0 grw-page-path-link">{pagePathHierarchicalLink}</h3>
+        <RevisionPathControls
+          pageId={pageId}
+          pagePath={path}
+        />
+      </span>
+      <TagLabels />
+    </div>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PagePathNavForEditorWrapper = (props) => {
+  return createSubscribedElement(PagePathNavForEditor, props, [AppContainer, PageContainer]);
+};
+
+
+PagePathNavForEditor.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(PagePathNavForEditorWrapper);

+ 7 - 15
src/client/js/components/PageList/PageListMeta.jsx

@@ -1,25 +1,17 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import templateChecker from '@commons/util/template-checker';
+import { isTopPage } from '@commons/util/path-utils';
 
 export default class PageListMeta extends React.Component {
 
-  isPortalPath(path) {
-    if (path.match(/.*\/$/)) {
-      return true;
-    }
-
-    return false;
-  }
-
   render() {
-    // TODO isPortal()
-    const page = this.props.page;
+    const { page } = this.props;
 
-    // portal check
-    let portalLabel;
-    if (this.isPortalPath(page.path)) {
-      portalLabel = <span className="badge badge-info">PORTAL</span>;
+    // top check
+    let topLabel;
+    if (isTopPage(page.path)) {
+      topLabel = <span className="badge badge-info">TOP</span>;
     }
 
     // template check
@@ -45,7 +37,7 @@ export default class PageListMeta extends React.Component {
 
     return (
       <span className="page-list-meta">
-        {portalLabel}
+        {topLabel}
         {templateLabel}
         {commentCount}
         {likerCount}

+ 8 - 31
src/client/js/legacy/crowi.js

@@ -2,8 +2,6 @@
 
 import { pathUtils } from 'growi-commons';
 
-const entities = require('entities');
-const escapeStringRegexp = require('escape-string-regexp');
 require('jquery.cookie');
 
 require('./thirdparty-js/waves');
@@ -261,12 +259,12 @@ $(() => {
     return false;
   });
 
-  // rename/unportalize
-  $('#renamePage, #unportalize').on('shown.bs.modal', (e) => {
+  // rename
+  $('#renamePage').on('shown.bs.modal', (e) => {
     $('#renamePage #newPageName').focus();
-    $('#renamePage .msg, #unportalize .msg').hide();
+    $('#renamePage .msg').hide();
   });
-  $('#renamePageForm, #unportalize-form').submit(function(e) {
+  $('#renamePageForm').submit(function(e) {
     // create name-value map
     const nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
@@ -284,9 +282,9 @@ $(() => {
       // error
         if (!res.ok) {
           const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-          $('#renamePage .msg, #unportalize .msg').hide();
-          $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
-          $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
+          $('#renamePage .msg').hide();
+          $(`#renamePage .msg-${res.code}`).show();
+          $('#renamePage #linkToNewPage').html(`
           <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
         `);
         }
@@ -304,7 +302,7 @@ $(() => {
     $('#duplicatePage #duplicatePageName').focus();
     $('#duplicatePage .msg').hide();
   });
-  $('#duplicatePageForm, #unportalize-form').submit(function(e) {
+  $('#duplicatePageForm').submit(function(e) {
     // create name-value map
     const nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
@@ -459,27 +457,6 @@ $(() => {
     window.location.hash = '#';
   });
 
-  /*
-   * wrap short path with <strong></strong>
-   */
-  $('#view-list .page-list-ul-flat .page-list-link').each(function() {
-    const $link = $(this);
-    /* eslint-disable-next-line no-unused-vars */
-    const text = $link.text();
-    let path = decodeURIComponent($link.data('path'));
-    const shortPath = decodeURIComponent($link.data('short-path')); // convert to string
-
-    if (path == null || shortPath == null) {
-      // continue
-      return;
-    }
-
-    path = entities.encodeHTML(path);
-    const pattern = `${escapeStringRegexp(entities.encodeHTML(shortPath))}(/)?$`;
-
-    $link.html(path.replace(new RegExp(pattern), `<strong>${shortPath}$1</strong>`));
-  });
-
   if (pageId) {
     // for Crowi Template LangProcessor
     $('.template-create-button', $('#revision-body')).on('click', function() {

+ 0 - 5
src/client/styles/scss/_comment_crowi.scss

@@ -1,5 +0,0 @@
-.crowi {
-  .page-comment-main {
-    margin-bottom: 0.5em;
-  }
-}

+ 0 - 38
src/client/styles/scss/_layout_crowi.scss

@@ -1,38 +0,0 @@
-.crowi {
-  header {
-    #search-listpage-form {
-      .btn-sm {
-        line-height: 0.5em;
-      }
-    }
-  }
-
-  .revision-toc {
-    float: right;
-    max-width: 250px;
-    overflow: hidden;
-    border: solid 1px #aaa;
-    border-radius: 5px;
-
-    .revision-toc-head {
-      display: inline-block;
-      float: right;
-      padding: 3px 11px;
-      margin-bottom: 5px;
-      margin-left: 5px;
-      font-weight: bold;
-      background: $gray-200;
-      border-bottom: solid 1px #aaa;
-      border-left: solid 1px #aaa;
-      border-radius: 0 5px;
-
-      &.collapsed {
-        margin: 0;
-        border: none;
-      }
-    }
-    .revision-toc-content {
-      background: #fcfcfc;
-    }
-  }
-}

+ 0 - 201
src/client/styles/scss/_layout_crowi_sidebar.scss

@@ -1,201 +0,0 @@
-.crowi-sidebar {
-  position: fixed;
-  top: 0;
-  right: 0;
-  height: 100%;
-  padding: 65px 0 0 0;
-  overflow: auto;
-  border-left: solid 1px transparent;
-
-  transition: 0.3s ease;
-
-  .page-meta {
-    padding: 15px 15px 5px 15px;
-    font-size: 0.9em;
-
-    line-height: 1.4em;
-    border-bottom: solid 1px #ccc;
-    p {
-      line-height: 1.4em;
-    }
-
-    .creator-picture {
-      text-align: center;
-      img {
-        width: 48px;
-        height: 48px;
-        border: 1px solid #ccc;
-      }
-    }
-    .creator {
-      font-size: 1.3em;
-      font-weight: bold;
-    }
-    .created-at {
-    }
-
-    .like-box {
-      padding-bottom: 0;
-
-      .dl-horizontal {
-        margin-bottom: 0;
-
-        dt,
-        dd {
-          padding-top: 5px;
-          padding-bottom: 5px;
-          border-top: solid 1px #ccc;
-        }
-        dt {
-          width: 80px;
-        }
-        dd {
-          margin-left: 90px;
-          text-align: right;
-        }
-      }
-    }
-
-    .liker-user-count,
-    .seen-user-count {
-      margin-bottom: 5px;
-      font-size: 1.2em;
-      font-weight: bold;
-    }
-  }
-
-  .side-content {
-    padding: 15px;
-    margin-bottom: 100px;
-
-    h3 {
-      font-size: 1.1em;
-    }
-
-    ul.fitted-list {
-      padding-left: 0;
-      li {
-        margin-bottom: 2px;
-
-        .input-group-addon {
-          padding: 5px 6px;
-        }
-      }
-    }
-
-    .page-comments {
-      margin: 8px 0 0 0;
-
-      .page-comment-form {
-        margin-top: 16px;
-
-        .comment-form-main {
-          .navbar-editor button {
-            padding: 5px;
-            font-size: 12px;
-          }
-          .overlay-gfm-cheatsheet {
-            display: none; // hide cheatsheet
-          }
-          .CodeMirror {
-            height: 150px;
-          }
-          .comment-submit {
-            margin-top: 8px;
-          }
-        }
-      }
-
-      hr {
-        border-color: #ccc;
-      }
-
-      .page-comments-list {
-        .page-comment {
-          padding-top: 8px;
-          margin-top: 8px;
-
-          .picture {
-            float: left;
-            width: 24px;
-            height: 24px;
-          }
-
-          .page-comment-creator {
-            font-weight: bold;
-          }
-
-          .page-comment-main {
-            position: relative;
-            margin-left: 40px;
-
-            .page-comment-meta {
-              font-size: 0.9em;
-              color: #aaa;
-            }
-            .page-comment-body {
-              padding: 8px 0;
-              word-wrap: break-word;
-            }
-            .page-comment-control {
-              position: absolute;
-              top: 0;
-              right: 0;
-              display: none; // default hidden
-            }
-          }
-        }
-      }
-    }
-  }
-
-  .portal-form-button {
-    text-align: center;
-  }
-
-  .system-version {
-    position: fixed;
-    right: 1.4em;
-    bottom: 0.1em;
-    z-index: 1;
-
-    display: flex;
-    justify-content: space-between;
-    width: calc(25% - 1.5em);
-    padding-right: 1em;
-    opacity: 1;
-
-    transition: 0.3s ease;
-  }
-}
-
-body:not(.aside-hidden) #toggle-crowi-sidebar {
-  i.ti-angle-left {
-    display: none;
-  }
-  i.ti-angle-right {
-    display: block;
-  }
-}
-
-.aside-hidden {
-  #toggle-crowi-sidebar {
-    right: 0;
-    i.ti-angle-right {
-      display: block;
-    }
-    i.ti-angle-right {
-      display: none;
-    }
-  }
-
-  .crowi-sidebar,
-  .system-version {
-    right: -25%;
-  }
-
-  .grw-subnav .col-md-9,
-  .main {
-    width: 100%;
-  }
-}

+ 28 - 25
src/client/styles/scss/_on-edit.scss

@@ -24,6 +24,11 @@ body.on-edit {
     }
   }
 
+  // show
+  .d-edit-block {
+    display: block !important;
+  }
+
   // hide unnecessary elements
   header,
   footer,
@@ -36,12 +41,6 @@ body.on-edit {
     display: none !important;
   }
 
-  // hide unnecessary elements for crowi layout
-  #toggle-crowi-sidebar,
-  .crowi-sidebar {
-    display: none;
-  }
-
   // show only either Edit button or HackMD button
   &.hackmd .nav-tab-edit {
     display: none;
@@ -69,11 +68,6 @@ body.on-edit {
     }
   }
 
-  // show revision path
-  .grw-revision-path-for-edit {
-    display: block !important;
-  }
-
   /*****************
    * Expand Editor
    *****************/
@@ -99,26 +93,23 @@ body.on-edit {
       padding: 0; //    for crowi layout
       pointer-events: initial; // enable pointer-events
     }
+  }
 
-    h1#revision-path {
-      @include variable-font-size(20px);
+  .grw-page-path-nav-for-edit {
+    .grw-page-path-link {
+      font-size: 20px;
       line-height: 1em;
-
-      // nowrap even if the path is too long
-      .d-flex {
-        flex-wrap: nowrap;
-      }
-
-      .path-segment {
-        white-space: nowrap;
-      }
     }
-
-    .tag-labels.new-page {
-      display: block;
+    .separator {
+      margin-right: 0.1em;
+      margin-left: 0.1em;
     }
   }
 
+  .tag-labels {
+    line-height: 1em;
+  }
+
   .page-editor-footer {
     width: 100%;
     min-height: 40px;
@@ -142,6 +133,18 @@ body.on-edit {
     }
   }
 
+  /*********************
+   * Navigation styles
+   */
+  .nav:hover {
+    .btn-copy,
+    .btn-edit,
+    .btn-edit-tags {
+      // change button opacity
+      opacity: unset;
+    }
+  }
+
   &.builtin-editor {
     /*****************
     * Editor styles

+ 0 - 4
src/client/styles/scss/_page.scss

@@ -24,10 +24,6 @@
       }
     }
   }
-
-  .tag-labels.new-page {
-    display: none;
-  }
 }
 
 .main .content-main .revision-history {

+ 2 - 1
src/client/styles/scss/_staff_credit.scss

@@ -2,8 +2,9 @@
 .staff-credit {
   // attached !important for updating from .modal-dialog class style
   width: 80vw !important;
-  height: 80vh !important;
   max-width: initial !important;
+  height: 80vh !important;
+
   // see https://css-tricks.com/old-timey-terminal-styling/
   @mixin old-timey-terminal-styling() {
     text-shadow: 0 0 10px #c8c8c8;

+ 7 - 0
src/client/styles/scss/_subnav.scss

@@ -77,6 +77,13 @@ header.grw-header {
     line-height: 1.1em;
   }
 
+  .grw-page-path-nav {
+    .separator {
+      margin-right: 0.2em;
+      margin-left: 0.2em;
+    }
+  }
+
   ul.authors {
     padding-left: 1.5em;
     margin: 0;

+ 19 - 0
src/client/styles/scss/molecules/copy-dropdown.scss

@@ -0,0 +1,19 @@
+.grw-copy-dropdown {
+  .dropdown-menu {
+    .dropdown-header {
+      margin-bottom: 0.5em;
+      font-size: 1.1em;
+    }
+
+    // unset active styles
+    .dropdown-item:active {
+      color: unset;
+      background-color: unset;
+    }
+
+    .well {
+      font-size: 0.6em;
+      word-break: break-all;
+    }
+  }
+}

+ 3 - 3
src/client/styles/scss/style-app.scss

@@ -20,11 +20,13 @@
 @import 'atoms/spinners';
 @import 'atoms/custom_control';
 
+// molecules
+@import 'molecules/copy-dropdown';
+
 // growi component
 @import 'admin';
 @import 'attachments';
 @import 'comment';
-@import 'comment_crowi';
 @import 'comment_growi';
 @import 'comment_kibela';
 @import 'drawio';
@@ -35,8 +37,6 @@
 @import 'editor-navbar';
 @import 'handsontable';
 @import 'layout';
-@import 'layout_crowi';
-@import 'layout_crowi_sidebar';
 @import 'layout_growi';
 @import 'layout_kibela';
 @import 'layout_variable';

+ 1 - 0
src/client/styles/scss/theme/_apply-colors.scss

@@ -7,6 +7,7 @@ $body-bg: $bgcolor-global;
 $body-color: $color-global;
 $link-color: $color-link;
 $link-hover-color: $color-link-hover;
+$input-focus-color: $color-global;
 
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/variables';

+ 189 - 6
src/client/styles/scss/theme/mono-blue.scss

@@ -1,8 +1,191 @@
-// import colors
-@import '../../agile-admin/inverse/colors/mono-blue';
+@import '../variables';
+@import '../override-bootstrap-variables';
 
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
 
-// override
-@import 'override-agileadmin';
+
+html[light] {
+  // Theme colors
+  $themecolor: #00587a;
+  $themelight: #f7fbfd;
+  $accentcolor: #16617d;
+  $subthemecolor: #186718;
+
+  $primary: $themecolor;
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-navbar: $themecolor;
+  $bgcolor-inline-code: lighten($subthemecolor, 70%);
+  $bgcolor-card: darken($themelight, 5%);
+
+  // Font colors
+  $color-global: $themecolor;
+  $color-reversal: #eee;
+  $color-link: lighten($primary, 5%);
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: lighten($primary, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $subthemecolor;
+  $color-search: #c0d6df;
+
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: $themecolor;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 30%), 20%);
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #ccc;
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-reversal;
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $bgcolor-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
+
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  // Navs {
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        background-color: transparent;
+      }
+    }
+  }
+
+  // Search Top
+  .search-top {
+    .input-group-prepend .dropdown-toggle {
+      color: $themecolor;
+      background-color: $color-search;
+      &:hover {
+        background-color: darken($color-search, 10%);
+      }
+    }
+  }
+}
+
+html[dark] {
+  // Theme colors
+  $themecolor: #0090c8;
+  $themedark: #061f2f;
+  $accentcolor: #16617d;
+  $subthemecolor: #c1f1f0;
+
+  $primary: $themecolor;
+  $dark: #031018;
+
+  // Background colors
+  $bgcolor-global: $themedark;
+  $bgcolor-navbar: #27343b;
+  $bgcolor-inline-code: #0a121b;
+  $bgcolor-card: darken($themedark, 5%);
+
+  // Font colors
+  $color-global: #d3d4d4;
+  $color-reversal: #eee;
+  $color-link: #97d1f0;
+  $color-link-hover: darken($color-link, 12%);
+  $color-link-wiki: lighten($primary, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $subthemecolor;
+  $color-search: #000102;
+
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: #13191c;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+  // $fillcolor-logo-mark: #4e5a60;
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #146aa0;
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-reversal;
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $bgcolor-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
+
+
+  @import 'apply-colors';
+  @import 'apply-colors-dark';
+
+  // Navs {
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        color: $color-link;
+        background-color: transparent;
+        border-color: $border-color-theme;
+      }
+    }
+  }
+
+  // Search Top
+  .search-top {
+    .input-group-prepend .dropdown-toggle {
+      background-color: $color-search;
+      border-color: $color-search;
+      &:hover {
+        background-color: darken($color-search, 10%);
+      }
+    }
+  }
+
+  // Table
+  .table {
+    color: white;
+  }
+}

+ 174 - 8
src/client/styles/scss/theme/nature.scss

@@ -1,8 +1,174 @@
-// import colors
-@import '../../agile-admin/inverse/colors/nature';
-
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
-
-// override
-@import 'override-agileadmin';
+@import '../variables';
+@import '../override-bootstrap-variables';
+
+// == Define Bootstrap theme colors
+//
+
+// colors for overriding bootstrap $theme-colors
+// $secondary: #;
+// $info: #;
+// $success: #;
+// $warning: #;
+// $danger: #;
+// $light: #;
+// $dark: #;
+
+.growi:not(.login-page) {
+  // add background-image
+  #page-wrapper,
+  .page-editor-preview-container {
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+.growi.login-page {
+  #page-wrapper {
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+$themecolor: #118050;
+$themelight: #fefffd;
+
+//== Light Mode
+//
+html[light],
+html[dark] {
+  $bgcolor-theme: #460039;
+
+  $bgcolor-navbar: #118050;
+  $bgcolor-global: #fefffd;
+
+  $color-header: #46694e;
+  $color-global: #333333;
+  $linktext: lighten($bgcolor-theme, 5%);
+  $linktext-hover: lighten($linktext, 12%);
+  $sidebar-text: #5c7253;
+
+  $primary: $bgcolor-theme;
+
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 30%), 20%);
+  $color-link-wiki: lighten($bgcolor-theme, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-navbar: $themecolor;
+  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-card: #f5f5f5;
+
+  // Font colors
+  $color-global: $bgcolor-theme;
+  $color-reversal: #eeeeee;
+  $color-link: lighten($color-global, 20%);
+  $color-link-hover: lighten($color-link, 20%);
+  $color-link-wiki: lighten($primary, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e;
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: $bgcolor-global;
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: $bgcolor-navbar;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #ccc;
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $growi-blue;
+  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-hover: $color-global;
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $bgcolor-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  .table {
+    background-color: $themelight;
+  }
+}
+
+//== Dark Mode
+//
+// html[dark] {
+//   $primary: #d65a31;
+
+//   $basecolor: #222831;
+
+//   // Background colors
+//   $bgcolor-global: $basecolor;
+//   $bgcolor-navbar: #151515;
+//   $bgcolor-inline-code: darken($basecolor, 5%);
+//   $bgcolor-card: darken($basecolor, 5%);
+
+//   // Font colors
+//   $color-global: #eeeeee;
+//   $color-reversal: #333333;
+//   // $color-header: desaturate($primary, 20%);
+//   $color-link: $primary;
+//   $color-link-hover: lighten($color-link, 10%);
+//   $color-link-wiki: lighten($basecolor, 50%);
+//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
+//   $color-link-nabvar: $color-global;
+//   $color-inline-code: #c7254e;
+
+//   // List Group colors
+//   $color-list: $color-global;
+//   $bgcolor-list: $bgcolor-global;
+//   $color-list-active: $color-reversal;
+//   $bgcolor-list-active: $primary;
+//   $color-list-hover: $color-reversal;
+
+//   // Logo colors
+//   $bgcolor-logo: $bgcolor-navbar;
+//   $fillcolor-logo-mark: #444;
+
+//   // Icon colors
+//   $color-editor-icons: darken($accentcolor, 15%);
+
+//   // Border colors
+//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
+
+//   // Dropdown colors
+//   $bgcolor-dropdown-link-active: $primary;
+//   $color-dropdown-link-active: $color-global;
+//   $color-dropdown-link-hover: $color-reversal;
+
+//   // alert
+//   $color-alert: $color-global;
+
+//   // badge
+//   $color-badge: $color-global;
+
+//   // Sidebar
+//   $bgcolor-sidebar: $bgcolor-navbar;
+//   $color-sidebar-context: $color-global;
+//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
+
+//   @import 'apply-colors';
+//   @import 'apply-colors-dark';
+// }

+ 170 - 6
src/client/styles/scss/theme/spring.scss

@@ -1,8 +1,172 @@
-// import colors
-@import '../../agile-admin/inverse/colors/spring';
+@import '../variables';
+@import '../override-bootstrap-variables';
 
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
+// == Define Bootstrap theme colors
+//
 
-// override
-@import 'override-agileadmin';
+// colors for overriding bootstrap $theme-colors
+// $secondary: #;
+// $info: #;
+// $success: #;
+// $warning: #;
+// $danger: #;
+// $light: #;
+// $dark: #;
+
+.growi:not(.login-page) {
+  // add background-image
+  #page-wrapper,
+  .page-editor-preview-container {
+    background-image: url('/images/themes/spring/spring02.svg');
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+.growi.login-page {
+  #page-wrapper {
+    background-image: url('/images/themes/spring/spring.svg');
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+$themecolor: #ffb8c6;
+$themelight: #fff0f5;
+$subthemecolor: #67a856;
+$third-main-color: antiquewhite;
+$accentcolor: #e08dbc;
+
+.grw-navbar {
+  border-bottom: $accentcolor 4px solid;
+}
+
+//== Light Mode
+//
+html[light],
+html[dark] {
+  $primary: $themecolor;
+
+  // Background colors
+  $bgcolor-global: white;
+  $bgcolor-navbar: $themecolor;
+  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-card: #f5f5f5;
+
+  // Font colors
+  $color-global: black;
+  $color-reversal: #eeeeee;
+  // $color-header: #2b2b2b;
+  $color-link: lighten($color-global, 20%);
+  $color-link-hover: lighten($color-link, 20%);
+  $color-link-wiki: $subthemecolor;
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e;
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: $bgcolor-global;
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: $bgcolor-navbar;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $growi-blue;
+  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-hover: $color-global;
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $bgcolor-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  .table {
+    background-color: $bgcolor-global;
+  }
+  .card-timeline > .card-header {
+    background-color: $third-main-color;
+  }
+}
+
+//== Dark Mode
+//
+// html[dark] {
+//   $primary: #d65a31;
+
+//   $basecolor: #222831;
+
+//   // Background colors
+//   $bgcolor-global: $basecolor;
+//   $bgcolor-navbar: #151515;
+//   $bgcolor-inline-code: darken($basecolor, 5%);
+//   $bgcolor-card: darken($basecolor, 5%);
+
+//   // Font colors
+//   $color-global: #eeeeee;
+//   $color-reversal: #333333;
+//   // $color-header: desaturate($primary, 20%);
+//   $color-link: $primary;
+//   $color-link-hover: lighten($color-link, 10%);
+//   $color-link-wiki: lighten($basecolor, 50%);
+//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
+//   $color-link-nabvar: $color-global;
+//   $color-inline-code: #c7254e;
+
+//   // List Group colors
+//   $color-list: $color-global;
+//   $bgcolor-list: $bgcolor-global;
+//   $color-list-active: $color-reversal;
+//   $bgcolor-list-active: $primary;
+//   $color-list-hover: $color-reversal;
+
+//   // Logo colors
+//   $bgcolor-logo: $bgcolor-navbar;
+//   $fillcolor-logo-mark: #444;
+
+//   // Icon colors
+//   $color-editor-icons: darken($accentcolor, 15%);
+
+//   // Border colors
+//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
+
+//   // Dropdown colors
+//   $bgcolor-dropdown-link-active: $primary;
+//   $color-dropdown-link-active: $color-global;
+//   $color-dropdown-link-hover: $color-reversal;
+
+//   // alert
+//   $color-alert: $color-global;
+
+//   // badge
+//   $color-badge: $color-global;
+
+//   // Sidebar
+//   $bgcolor-sidebar: $bgcolor-navbar;
+//   $color-sidebar-context: $color-global;
+//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
+
+//   @import 'apply-colors';
+//   @import 'apply-colors-dark';
+// }

+ 167 - 6
src/client/styles/scss/theme/wood.scss

@@ -1,8 +1,169 @@
-// import colors
-@import '../../agile-admin/inverse/colors/wood';
+@import '../variables';
+@import '../override-bootstrap-variables';
 
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
+// == Define Bootstrap theme colors
+//
+// colors for overriding bootstrap $theme-colors
+// $secondary: #;
+// $success: #;
+// $warning: #;
+// $danger: #;
+// $light: #;
+// $dark: #;
 
-// override
-@import 'override-agileadmin';
+.growi:not(.login-page) {
+
+  // add background-image
+  #page-wrapper,
+  .page-editor-preview-container {
+    background-image: url('/images/themes/wood/wood.jpg');
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+.growi.login-page {
+  #page-wrapper {
+    background-image: url('/images/themes/wood/wood.jpg');
+    background-attachment: fixed;
+    background-position: center center;
+    background-size: cover;
+  }
+}
+
+$themecolor: #aaa45f;
+$themelight: #f5f3ee;
+$accentcolor: #577254;
+
+//== Light Mode
+//
+html[light],
+html[dark] {
+  $primary: $themecolor;
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-navbar: $themecolor;
+  $bgcolor-inline-code: darken($themecolor, 20%);
+  $bgcolor-card: #f5f5f5;
+
+  // Font colors
+  $color-global: black;
+  $color-reversal: #fffffc;
+  // $color-header: #2b2b2b;
+  $color-link: lighten($color-global, 20%);
+  $color-link-hover: lighten($color-link, 20%);
+  $color-link-wiki: lighten($themecolor, 5%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 15%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: lighten($accentcolor, 70%);
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: $bgcolor-global;
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: darken($themecolor, 10%);
+  $fillcolor-logo-mark: lighten(desaturate($themecolor, 50%), 50%); // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $growi-blue;
+  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-hover: $color-global;
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $bgcolor-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+
+  // portal
+  $info: lighten($themecolor, 10%);
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  .table {
+    background-color: $themelight;
+  }
+
+  .grw-navbar {
+    background-image: url('/images/themes/wood/wood-navbar.jpg');
+    border-bottom: $accentcolor 4px solid;
+
+  }
+}
+
+//== Dark Mode
+//
+// html[dark] {
+//   $primary: #d65a31;
+
+//   $basecolor: #222831;
+
+//   // Background colors
+//   $bgcolor-global: $basecolor;
+//   $bgcolor-navbar: #151515;
+//   $bgcolor-inline-code: darken($basecolor, 5%);
+//   $bgcolor-card: darken($basecolor, 5%);
+
+//   // Font colors
+//   $color-global: #eeeeee;
+//   $color-reversal: #333333;
+//   // $color-header: desaturate($primary, 20%);
+//   $color-link: $primary;
+//   $color-link-hover: lighten($color-link, 10%);
+//   $color-link-wiki: lighten($basecolor, 50%);
+//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
+//   $color-link-nabvar: $color-global;
+//   $color-inline-code: #c7254e;
+
+//   // List Group colors
+//   $color-list: $color-global;
+//   $bgcolor-list: $bgcolor-global;
+//   $color-list-active: $color-reversal;
+//   $bgcolor-list-active: $primary;
+//   $color-list-hover: $color-reversal;
+
+//   // Logo colors
+//   $bgcolor-logo: $bgcolor-navbar;
+//   $fillcolor-logo-mark: #444;
+
+//   // Icon colors
+//   $color-editor-icons: darken($accentcolor, 15%);
+
+//   // Border colors
+//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
+
+//   // Dropdown colors
+//   $bgcolor-dropdown-link-active: $primary;
+//   $color-dropdown-link-active: $color-global;
+//   $color-dropdown-link-hover: $color-reversal;
+
+//   // alert
+//   $color-alert: $color-global;
+
+//   // badge
+//   $color-badge: $color-global;
+
+//   // Sidebar
+//   $bgcolor-sidebar: $bgcolor-navbar;
+//   $color-sidebar-context: $color-global;
+//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
+
+//   @import 'apply-colors';
+//   @import 'apply-colors-dark';
+// }

+ 4 - 4
src/lib/components/PagePathHierarchicalLink.jsx

@@ -7,7 +7,7 @@ import LinkedPagePath from '../models/linked-page-path';
 
 
 const PagePathHierarchicalLink = (props) => {
-  const { linkedPagePath, basePath } = props;
+  const { linkedPagePath, basePath, isInTrash } = props;
 
   // render root element
   if (linkedPagePath.isRoot) {
@@ -15,7 +15,7 @@ const PagePathHierarchicalLink = (props) => {
       return null;
     }
 
-    return props.isPageInTrash
+    return isInTrash
       ? (
         <>
           <span className="path-segment">
@@ -37,7 +37,7 @@ const PagePathHierarchicalLink = (props) => {
   }
 
   const isParentExists = linkedPagePath.parent != null;
-  const isParentRoot = isParentExists && linkedPagePath.parent.isRoot;
+  const isParentRoot = linkedPagePath.parent?.isRoot;
   const isSeparatorRequired = isParentExists && !isParentRoot;
 
   const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
@@ -70,7 +70,7 @@ PagePathHierarchicalLink.propTypes = {
   // !!INTERNAL USE ONLY!!
   isInnerElem: PropTypes.bool,
 
-  isPageInTrash: PropTypes.bool, // TODO: omit
+  isInTrash: PropTypes.bool, // TODO: omit
 };
 
 export default PagePathHierarchicalLink;

+ 2 - 0
src/lib/models/devided-page-path.js

@@ -10,6 +10,7 @@ export default class DevidedPagePath {
   constructor(path, skipNormalize = false, evalDatePath = false) {
 
     this.isRoot = false;
+    this.isFormerRoot = false;
     this.former = null;
     this.latter = null;
 
@@ -35,6 +36,7 @@ export default class DevidedPagePath {
 
     const matchDefault = pagePath.match(PATTERN_DEFAULT);
     if (matchDefault != null) {
+      this.isFormerRoot = matchDefault[1] === '/';
       this.former = matchDefault[2];
       this.latter = matchDefault[3];
     }

+ 6 - 0
src/lib/models/linked-page-path.js

@@ -1,4 +1,5 @@
 import { pathUtils } from 'growi-commons';
+import { isTrashPage } from '@commons/util/path-utils';
 
 import DevidedPagePath from './devided-page-path';
 
@@ -11,6 +12,7 @@ export default class LinkedPagePath {
 
     const pagePath = new DevidedPagePath(path, skipNormalize);
 
+    this.path = path;
     this.pathName = pagePath.latter;
     this.isRoot = pagePath.isRoot;
     this.parent = pagePath.isRoot
@@ -27,4 +29,8 @@ export default class LinkedPagePath {
     return pathUtils.normalizePath(`${this.parent.href}/${this.pathName}`);
   }
 
+  get isInTrash() {
+    return isTrashPage(this.path);
+  }
+
 }

+ 9 - 0
src/lib/util/path-utils.js

@@ -1,3 +1,11 @@
+/**
+ * Whether path is the top page
+ * @param {string} path
+ * @returns {boolean}
+ */
+const isTopPage = (path) => {
+  return path === '/';
+};
 
 /**
  * Whether path belongs to the trash page
@@ -40,6 +48,7 @@ const userPageRoot = (user) => {
 };
 
 module.exports = {
+  isTopPage,
   isTrashPage,
   isUserPage,
   userPageRoot,

+ 38 - 0
src/migrations/20200512005851-remove-behavior-type.js

@@ -0,0 +1,38 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:remove-behavior-type');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+module.exports = {
+  async up(db, client) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down(db, client) {
+    // do not rollback
+    logger.info('Rollback migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    const insertConfig = new Config({
+      ns: 'crowi',
+      key: 'customize:behavior',
+      value: JSON.stringify('growi'),
+    });
+
+    await insertConfig.save();
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 1 - 4
src/server/models/config.js

@@ -22,7 +22,6 @@ module.exports = function(crowi) {
     // overwrite
     config['app:installed'] = true;
     config['app:fileUpload'] = true;
-    config['customize:behavior'] = 'growi';
     config['customize:layout'] = 'growi';
     config['customize:isSavedStatesOfTabChanges'] = false;
 
@@ -108,8 +107,7 @@ module.exports = function(crowi) {
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyleBorder' : false,
       'customize:theme' : 'default',
-      'customize:behavior' : 'crowi',
-      'customize:layout' : 'crowi',
+      'customize:layout' : 'growi',
       'customize:isEnabledTimeline' : true,
       'customize:isSavedStatesOfTabChanges' : true,
       'customize:isEnabledAttachTitleHeader' : false,
@@ -190,7 +188,6 @@ module.exports = function(crowi) {
         file: crowi.fileUploadService.getFileUploadEnabled(),
       },
       registrationWhiteList: crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
-      behaviorType: crowi.configManager.getConfig('crowi', 'customize:behavior'),
       layoutType: crowi.configManager.getConfig('crowi', 'customize:layout'),
       themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),

+ 4 - 11
src/server/models/page.js

@@ -14,6 +14,7 @@ const differenceInYears = require('date-fns/differenceInYears');
 
 const { pathUtils } = require('growi-commons');
 const templateChecker = require('@commons/util/template-checker');
+const { isTopPage } = require('@commons/util/path-utils');
 const escapeStringRegexp = require('escape-string-regexp');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
@@ -291,14 +292,6 @@ module.exports = function(crowi) {
     pageEvent.on('update', pageEvent.onUpdate);
   }
 
-  function isPortalPath(path) {
-    if (path.match(/.*\/$/)) {
-      return true;
-    }
-
-    return false;
-  }
-
   function validateCrowi() {
     if (crowi == null) {
       throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
@@ -317,8 +310,8 @@ module.exports = function(crowi) {
     return false;
   };
 
-  pageSchema.methods.isPortal = function() {
-    return isPortalPath(this.path);
+  pageSchema.methods.isTopPage = function() {
+    return isTopPage(this.path);
   };
 
   pageSchema.methods.isTemplate = function() {
@@ -991,7 +984,7 @@ module.exports = function(crowi) {
 
     let grant = options.grant;
     // force public
-    if (isPortalPath(path)) {
+    if (isTopPage(path)) {
       grant = GRANT_PUBLIC;
     }
 

+ 0 - 1
src/server/routes/apiv3/customize-setting.js

@@ -152,7 +152,6 @@ module.exports = (crowi) => {
     const customizeParams = {
       layoutType: await crowi.configManager.getConfig('crowi', 'customize:layout'),
       themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
-      behaviorType: await crowi.configManager.getConfig('crowi', 'customize:behavior'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),

+ 29 - 107
src/server/routes/page.js

@@ -146,16 +146,12 @@ module.exports = function(crowi, app) {
   const ApiResponse = require('../util/apiResponse');
   const getToday = require('../util/getToday');
 
-  const { configManager, slackNotificationService } = crowi;
+  const { slackNotificationService, configManager } = crowi;
   const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
 
   const actions = {};
 
-  const PORTAL_STATUS_NOT_EXISTS = 0;
-  const PORTAL_STATUS_EXISTS = 1;
-  const PORTAL_STATUS_FORBIDDEN = 2;
-
   // register page events
 
   const pageEvent = crowi.event('page');
@@ -338,31 +334,22 @@ module.exports = function(crowi, app) {
     return res.render('page_presentation', renderVars);
   }
 
-  async function showPageListForCrowiBehavior(req, res, next) {
-    const portalPath = pathUtils.addTrailingSlash(getPathFromRequest(req));
+  async function showTopPage(req, res, next) {
+    const portalPath = req.path;
     const revisionId = req.query.revision;
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
-    // check whether this page has portal page
-    const portalPageStatus = await getPortalPageState(portalPath, req.user);
-
-    let view = 'customlayout-selector/page_list';
+    const view = `layout-${layoutName}/page_list`;
     const renderVars = { path: portalPath };
 
-    if (portalPageStatus === PORTAL_STATUS_FORBIDDEN) {
-      // inject to req
-      req.isForbidden = true;
-      view = 'customlayout-selector/forbidden';
-    }
-    else if (portalPageStatus === PORTAL_STATUS_EXISTS) {
-      let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
-      portalPage.initLatestRevisionField(revisionId);
+    let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
+    portalPage.initLatestRevisionField(revisionId);
 
-      // populate
-      portalPage = await portalPage.populateDataToShowRevision();
+    // populate
+    portalPage = await portalPage.populateDataToShowRevision();
 
-      addRendarVarsForPage(renderVars, portalPage);
-      await addRenderVarsForSlack(renderVars, portalPage);
-    }
+    addRendarVarsForPage(renderVars, portalPage);
+    await addRenderVarsForSlack(renderVars, portalPage);
 
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
@@ -376,6 +363,7 @@ module.exports = function(crowi, app) {
   async function showPageForGrowiBehavior(req, res, next) {
     const path = getPathFromRequest(req);
     const revisionId = req.query.revision;
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     let page = await Page.findByPathAndViewer(path, req.user);
 
@@ -395,7 +383,7 @@ module.exports = function(crowi, app) {
     const offset = parseInt(req.query.offset) || 0;
     const renderVars = {};
 
-    let view = 'customlayout-selector/page';
+    let view = `layout-${layoutName}/page`;
 
     page.initLatestRevisionField(revisionId);
 
@@ -409,7 +397,7 @@ module.exports = function(crowi, app) {
 
     if (isUserPage(page.path)) {
       // change template
-      view = 'customlayout-selector/user_page';
+      view = `layout-${layoutName}/user_page`;
       await addRenderVarsForUserPage(renderVars, page, req.user);
     }
 
@@ -427,46 +415,16 @@ module.exports = function(crowi, app) {
     return channels;
   };
 
-  /**
-   *
-   * @param {string} path
-   * @param {User} user
-   * @returns {number} PORTAL_STATUS_NOT_EXISTS(0) or PORTAL_STATUS_EXISTS(1) or PORTAL_STATUS_FORBIDDEN(2)
-   */
-  async function getPortalPageState(path, user) {
-    const portalPath = Page.addSlashOfEnd(path);
-    const page = await Page.findByPathAndViewer(portalPath, user);
-
-    if (page == null) {
-      // check the page is forbidden or just does not exist.
-      const isForbidden = await Page.count({ path: portalPath }) > 0;
-      return isForbidden ? PORTAL_STATUS_FORBIDDEN : PORTAL_STATUS_NOT_EXISTS;
-    }
-    return PORTAL_STATUS_EXISTS;
-  }
-
-
   actions.showTopPage = function(req, res) {
-    return showPageListForCrowiBehavior(req, res);
+    return showTopPage(req, res);
   };
 
   /**
-   * switch action by behaviorType
+   * Redirect to the page without trailing slash
    */
-  /* eslint-disable no-else-return */
   actions.showPageWithEndOfSlash = function(req, res, next) {
-    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
-
-    if (behaviorType === 'crowi') {
-      return showPageListForCrowiBehavior(req, res, next);
-    }
-    else {
-      const path = getPathFromRequest(req); // end of slash should be omitted
-      // redirect and showPage action will be triggered
-      return res.redirect(path);
-    }
+    return res.redirect(pathUtils.removeTrailingSlash(req.path));
   };
-  /* eslint-enable no-else-return */
 
   /**
    * switch action
@@ -478,20 +436,6 @@ module.exports = function(crowi, app) {
     if (req.query.presentation) {
       return showPageForPresentation(req, res, next);
     }
-
-    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
-
-    // check whether this page has portal page
-    if (behaviorType === 'crowi') {
-      const portalPagePath = pathUtils.addTrailingSlash(getPathFromRequest(req));
-      const hasPortalPage = await Page.count({ path: portalPagePath }) > 0;
-
-      if (hasPortalPage) {
-        logger.debug('The portal page is found', portalPagePath);
-        return res.redirect(encodeURI(`${portalPagePath}?redirectFrom=${pathUtils.encodePagePath(req.path)}`));
-      }
-    }
-
     // delegate to showPageForGrowiBehavior
     return showPageForGrowiBehavior(req, res, next);
   };
@@ -501,16 +445,8 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.trashPageListShowWrapper = function(req, res) {
-    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
-
-    if (behaviorType === 'crowi') {
-      // Crowi behavior for '/trash/*'
-      return actions.deletedPageListShow(req, res);
-    }
-    else {
-      // redirect to '/trash'
-      return res.redirect('/trash');
-    }
+    // redirect to '/trash'
+    return res.redirect('/trash');
   };
   /* eslint-enable no-else-return */
 
@@ -519,16 +455,8 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.trashPageShowWrapper = function(req, res) {
-    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
-
-    if (behaviorType === 'crowi') {
-      // redirect to '/trash/'
-      return res.redirect('/trash/');
-    }
-    else {
-      // Crowi behavior for '/trash/*'
-      return actions.deletedPageListShow(req, res);
-    }
+    // Crowi behavior for '/trash/*'
+    return actions.deletedPageListShow(req, res);
   };
   /* eslint-enable no-else-return */
 
@@ -537,16 +465,8 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.deletedPageListShowWrapper = function(req, res) {
-    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
-
-    if (behaviorType === 'crowi') {
-      // Crowi behavior for '/trash/*'
-      return actions.deletedPageListShow(req, res);
-    }
-    else {
-      const path = `/trash${getPathFromRequest(req)}`;
-      return res.redirect(path);
-    }
+    const path = `/trash${getPathFromRequest(req)}`;
+    return res.redirect(path);
   };
   /* eslint-enable no-else-return */
 
@@ -554,18 +474,19 @@ module.exports = function(crowi, app) {
     const path = getPathFromRequest(req);
 
     const isCreatable = Page.isCreatableName(path);
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     let view;
     const renderVars = { path };
 
     if (!isCreatable) {
-      view = 'customlayout-selector/not_creatable';
+      view = `layout-${layoutName}/not_creatable`;
     }
     else if (req.isForbidden) {
-      view = 'customlayout-selector/forbidden';
+      view = `layout-${layoutName}/forbidden`;
     }
     else {
-      view = 'customlayout-selector/not_found';
+      view = `layout-${layoutName}/not_found`;
 
       // retrieve templates
       if (req.user != null) {
@@ -596,6 +517,7 @@ module.exports = function(crowi, app) {
   actions.deletedPageListShow = async function(req, res) {
     // normalizePath makes '/trash/' -> '/trash'
     const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
@@ -620,7 +542,7 @@ module.exports = function(crowi, app) {
 
     renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
     renderVars.pages = pathUtils.encodePagesPath(result.pages);
-    res.render('customlayout-selector/page_list', renderVars);
+    res.render(`layout-${layoutName}/page_list`, renderVars);
   };
 
   /**

+ 0 - 7
src/server/views/customlayout-selector/forbidden.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/forbidden.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/forbidden.html' %}
-{% else %}
-  {% include '../layout-growi/forbidden.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/not_creatable.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/not_creatable.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/not_creatable.html' %}
-{% else %}
-  {% include '../layout-growi/not_creatable.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/not_found.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/not_found.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/not_found.html' %}
-{% else %}
-  {% include '../layout-growi/not_found.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/page.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/page.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/page.html' %}
-{% else %}
-  {% include '../layout-growi/page.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/page_list.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/page_list.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/page_list.html' %}
-{% else %}
-  {% include '../layout-growi/page_list.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/user_page.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/user_page.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/user_page.html' %}
-{% else %}
-  {% include '../layout-growi/user_page.html' %}
-{% endif %}

+ 0 - 58
src/server/views/layout-crowi/base/layout.html

@@ -1,58 +0,0 @@
-{% extends '../../layout/layout.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitle(path) }}{% endblock %}
-
-{% block html_additional_headers %}
-  {% parent %}
-  {{ cdnScriptTag('highlight-addons') }}
-  {{ cdnScriptTag('drawio-viewer') }}
-{% endblock %}
-
-{% block layout_main %}
-<div class="container-fluid">
-
-  <a href="" class=" hidden-xs hidden-sm layout-control" id="toggle-crowi-sidebar">
-    <i class="ti-angle-right"></i><i class="ti-angle-left"></i> <span class="hide-on-affix-top"></span>
-  </a>
-  <aside class="crowi-sidebar col-md-3 hidden-xs hidden-sm hidden-print">
-
-    {% block side_header %}
-    {% endblock %}
-
-    <div class="side-content">
-      {% block side_content %}
-      {% endblock %}
-    </div>
-
-    {% block side_footer %}
-    {% endblock %}
-
-    {% include '../../widget/system-version.html' %}
-  </aside>
-
-  <div class="row grw-subnav d-edit-none">
-    <div class="col-md-9">
-      {% block content_header %}
-      {% endblock %}
-    </div>
-  </div><!-- /.grw-subnav -->
-
-  <div class="row">
-    <div id="main" class="main col-md-9 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-      {% block content_main_before %}
-      {% endblock %}
-
-      {% block content_main %}
-      {% endblock content_main %}
-
-      {% block content_main_after %}
-      {% endblock %}
-
-      {% block content_footer %}
-      {% endblock %}
-
-    </div>
-  </div>
-
-</div><!-- /.container-fluid -->
-{% endblock %} {# layout_main #}

+ 0 - 40
src/server/views/layout-crowi/forbidden.html

@@ -1,40 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_header %}
-
-  {% block content_header_before %}
-  {% endblock %}
-
-  <header id="page-header">
-    <div>
-      <div>
-        <h1 class="title" id="revision-path"></h1>
-        {% if page and not forbidden and not isTrashPage() %}
-          <div id="tag-label"></div>
-        {% endif %}
-      </div>
-    </div>
-  </header>
-
-  {% block content_header_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  {% include '../widget/forbidden_content.html' %}
-{% endblock %}
-
-
-{% block content_main_after %}
-{% endblock %}
-
-
-{% block content_footer %}
-{% endblock %}

+ 0 - 40
src/server/views/layout-crowi/not_creatable.html

@@ -1,40 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_header %}
-
-  {% block content_header_before %}
-  {% endblock %}
-
-  <header id="page-header">
-    <div>
-      <div>
-        <h1 class="title" id="revision-path"></h1>
-        {% if page and not forbidden and not isTrashPage() %}
-          <div id="tag-label"></div>
-        {% endif %}
-      </div>
-    </div>
-  </header>
-
-  {% block content_header_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  {% include '../widget/not_creatable_content.html' %}
-{% endblock %}
-
-
-{% block content_main_after %}
-{% endblock %}
-
-
-{% block content_footer %}
-{% endblock %}

+ 0 - 40
src/server/views/layout-crowi/not_found.html

@@ -1,40 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_header %}
-
-  {% block content_header_before %}
-  {% endblock %}
-
-  <header id="page-header">
-    <div>
-      <div>
-        <h1 class="title" id="revision-path"></h1>
-        {% if not forbidden and not isTrashPage() %}
-          <div id="tag-label"></div>
-        {% endif %}
-      </div>
-    </div>
-  </header>
-
-  {% block content_header_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  {% include '../widget/not_found_content.html' %}
-{% endblock %}
-
-
-{% block content_main_after %}
-{% endblock %}
-
-
-{% block content_footer %}
-{% endblock %}

+ 0 - 69
src/server/views/layout-crowi/page.html

@@ -1,69 +0,0 @@
-{% extends 'base/layout.html' %}
-
-
-{% block content_header %}
-
-  {% block content_header_before %}
-  {% endblock %}
-
-  <header id="page-header">
-    <div class="d-flex align-items-center">
-      <div class="title-container">
-        <h1 class="title" id="revision-path"></h1>
-        {% if page and not forbidden and not isTrashPage() %}
-          <div id="tag-label"></div>
-        {% endif %}
-      </div>
-    </div>
-  </header>
-
-  {% block content_header_after %}
-  {% endblock %}
-
-{% endblock %}
-
-
-{% block content_main_before %}
-{% endblock %}
-
-
-{% block content_main %}
-  <div class="m-b-30">
-    {% include '../widget/page_content.html' %}
-  </div>
-{% endblock %}
-
-
-{% block content_main_after %}
-{% endblock %}
-
-
-{% block content_footer %}
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
-{% endblock %}
-
-{% block side_header %}
-  {% if page and not page.isDeleted() %}
-    {% include 'widget/page_side_header.html' %}
-  {% endif %}
-{% endblock %} {# side_header #}
-
-{% block side_content %}
-  {% if page and not page.isDeleted() %}
-    {% include 'widget/page_side_content.html' %}
-  {% endif %}
-{% endblock %}
-
-{% block layout_footer %}
-{% endblock %}
-
-{% block body_end %}
-  <div id="presentation-layer" class="fullscreen-layer">
-    <div id="presentation-container"></div>
-  </div>
-  <div id="crowi-modals">
-    {% include '../widget/page_modals.html' %}
-  </div>
-{% endblock %}

+ 0 - 83
src/server/views/layout-crowi/page_list.html

@@ -1,83 +0,0 @@
-{% extends 'base/layout.html' %}
-
-
-{% block html_base_attr %}
-  data-spy="scroll"
-  data-target="#search-result-list"
-{% endblock %}
-
-{% block content_header %}
-
-{% block content_header_before %}
-{% endblock %}
-
-<header id="page-header" class="{% if page %}has-page{% endif %}">
-
-  <div class="d-flex align-items-center">
-    <div class="title-container">
-      <h1 class="title" id="revision-path"></h1>
-      {% if page and not forbidden and not isTrashPage() %}
-        <div id="tag-label"></div>
-      {% endif %}
-    </div>
-  </div>
-
-</header>
-
-{% endblock %}
-
-{% block content_main %}
-
-  {% block content_main_before %}
-  {% endblock %}
-
-  {# page-list-search should be fully managed by react.js,
-  # but now the header and page list content is rendered separately by the server,
-  # so now bind the values through the hidden fields.
-  #}
-  {% if false %} {# Disable temporaly -- 2018.03.08 Yuki Takei #}
-  {% if isSearchServiceConfigured() && !isTopPage() && !isTrashPage() %}
-  <div id="page-list-search">
-  </div>
-  {% endif %}
-  {% endif %}
-
-  <div>
-    {% include '../widget/page_content.html' %}
-  </div>
-
-  <div class="row page-list d-edit-none d-print-none {% if page.isPortal() %}mt-5{% endif %}">
-    <div class="col-md-12">
-      {% include '../widget/page_list_and_timeline.html' %}
-    </div>
-  </div>
-
-{% endblock %}
-
-
-{% block content_main_after %}
-{% endblock %}
-
-
-{% block content_footer %}
-<footer>
-</footer>
-{% endblock %}
-
-
-{% block side_header %}
-
-{% if not page and not isUserPageList(path) and !isTrashPage() %}
-  {% include '../widget/create_portal.html' %}
-{% else %}
-  {% include 'widget/page_side_header.html' %}
-{% endif %}
-
-{% endblock %} {# side_header #}
-
-{% block body_end %}
-<div id="crowi-modals">
-  {% include '../modal/what_is_portal.html' %}
-  {% include '../modal/unportalize.html' %}
-</div>
-{% endblock %} {# body_end #}

+ 0 - 19
src/server/views/layout-crowi/user_page.html

@@ -1,19 +0,0 @@
-{% extends 'page.html' %}
-
-{% block main_css_class %}user-page{% endblock %}
-
-
-{% block content_header %}
-  {% if pageUser %}
-    {% include '../widget/user_page_header.html' %}
-  {% else %}
-    {% parent %}
-  {% endif %}
-{% endblock %}
-
-
-{% block content_main_before %}
-  <div class="m-b-30 user-page-content-container d-edit-none hidden-print">
-    {% include '../widget/user_page_content.html' %}
-  </div>
-{% endblock %}

+ 0 - 23
src/server/views/layout-crowi/widget/page_side_content.html

@@ -1,23 +0,0 @@
-<h3><i class="icon-link"></i> {{ t('Share') }}</h3>
-<ul class="fitted-list">
-  <li class="input-group">
-    <span class="input-group-addon">{{ t('Share Link') }}</span>
-    <input readonly class="copy-link form-control" type="text" value="{{ appTitle }} {{ path }} {{ baseUrl }}/{{ page._id.toString() }}">
-  </li>
-  <li class="input-group">
-    <span class="input-group-addon">Markdown</span>
-    <input readonly class="copy-link form-control" type="text" value="[{{ path }}]({{ baseUrl }}/{{ page._id.toString() }})">
-  </li>
-</ul>
-
-<h3><i class="icon-bubble"></i> Comments</h3>
-<div class="page-comments">
-  {% if page and not page.isDeleted() %}
-  <div id="page-comment-write"></div>
-  <hr>
-  {% endif %}
-
-  <div id="page-comment-form-behavior"></div>
-
-  <div class="page-comments-list" id="page-comments-list"></div>
-</div>

+ 0 - 43
src/server/views/layout-crowi/widget/page_side_header.html

@@ -1,43 +0,0 @@
-{% if page %} {# {{{ if page #}
-<div class="page-meta">
-  <div class="row">
-    {# default(author) としているのは、v1.1.1 以前に page.creator データが入ってないから。暫定として最新更新ユーザーを表示しちゃう。 #}
-    <div class="col-md-3 creator-picture">
-      <a href="{{ userPageRoot(page.creator) }}">
-        <img src="{{ page.creator|default(author)|picture }}" class="picture picture-lg rounded-circle"><br>
-      </a>
-    </div>
-    <div class="col-md-9">
-      <p class="creator">
-        <a href="{{ userPageRoot(page.creator) }}">{{ page.creator.name|default(author.name) }}</a>
-      </p>
-      <p class="created-at">
-        {{ t('Created') }}: {{ page.createdAt|datetz('Y/m/d H:i:s') }}<br>
-        {{ t('Last updated') }}: {{ page.updatedAt|datetz('Y/m/d H:i:s') }} <a href="/user/{{ page.revision.author.username }}"><img src="{{ page.revision.author|picture }}" class="picture picture-xs rounded-circle" alt="{{ page.revision.author.name }}"></a>
-      </p>
-    </div>
-  </div>
-
-  <span class="like-box">
-    <dl class="dl-horizontal">
-      <dt class="text-info">
-        <i class="icon-like"></i> {{ t('Like!') }}
-      </dt>
-      <dd>
-        <p class="liker-user-count">{{ page.liker.length|default(0) }}</p>
-        {% if page.liker.length > 15 %}<span class="text-muted">..</span>{% endif %}
-        <span id="liker-list" data-user-ids="{{ page.liker|slice(-15)|default([])|join(',') }}"></span>
-      </dd>
-
-      <dt class="text-danger">
-        <i class="fa fa-paw"></i> {{ t('Seen by') }}
-      </dt>
-      <dd>
-        <p class="seen-user-count">{{ page.seenUsers.length|default(0) }}</p>
-        {% if page.seenUsers.length > 15 %}<span class="text-muted">..</span>{% endif %}
-        <span id="seen-user-list" data-user-ids="{{ page.seenUsers|slice(-15)|default([])|join(',') }}"></span>
-      </dd>
-    </dl>
-  </div>
-</iv>
-{% endif %} {# if page }}} #}

+ 0 - 2
src/server/views/layout-growi/page.html

@@ -34,13 +34,11 @@
 
   </div>
 
-  {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
   <div class="row page-list d-edit-none d-print-none mt-5">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>
   </div>
-  {% endif %}
 {% endblock %}
 
 

+ 1 - 3
src/server/views/layout-growi/page_list.html

@@ -34,7 +34,7 @@
 
   </div>
 
-  <div class="row page-list d-edit-none d-print-none {% if page.isPortal() %}mt-5{% endif %}">
+  <div class="row page-list d-edit-none d-print-none {% if page.isTopPage() %}mt-5{% endif %}">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>
@@ -55,7 +55,5 @@
   </div>
   <div id="crowi-modals">
     {% include '../widget/page_modals.html' %}
-    {% include '../modal/what_is_portal.html' %}
-    {% include '../modal/unportalize.html' %}
   </div>
 {% endblock %}

+ 0 - 2
src/server/views/layout-growi/user_page.html

@@ -60,13 +60,11 @@
 
   </div>
 
-  {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
   <div class="row page-list d-edit-none d-print-none mt-5">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>
   </div>
-  {% endif %}
 
 {% endblock %}
 

+ 0 - 6
src/server/views/layout-growi/widget/header.html

@@ -1,7 +1 @@
 <div id="grw-subnav" class="grw-subnav d-edit-none" data-is-forbidden-page="{{ forbidden }}"></div>
-
-{% if not page and not forbidden and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
-  {% if '/' === path.slice(-1) %}
-    {% include '../../widget/create_portal.html' %}
-  {% endif %}
-{% endif %}

+ 1 - 0
src/server/views/layout-kibela/not_found.html

@@ -16,6 +16,7 @@
     <div class="col bg-white round-corner">
       {% include '../widget/not_found_content.html' %}
     </div>
+    <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container"></div>
   </div>
 {% endblock %}
 

+ 0 - 2
src/server/views/layout-kibela/page.html

@@ -32,13 +32,11 @@
 
 </div>
 
-  {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
   <div class="row page-list grw-pt-10px my-5 round-corner d-edit-none">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>
   </div>
-  {% endif %}
 {% endblock %}
 
 

+ 1 - 3
src/server/views/layout-kibela/page_list.html

@@ -32,7 +32,7 @@
 
 </div>
 
-  <div class="row page-list bg-white round-corner grw-pt-10px mb-5 d-edit-none {% if page.isPortal() %}mt-5{% endif %}">
+  <div class="row page-list bg-white round-corner grw-pt-10px my-5 d-edit-none {% if page.isTopPage() %}mt-5{% endif %}">
     <div class="col">
       {% include '../widget/page_list_and_timeline_kibela.html' %}
     </div>
@@ -53,7 +53,5 @@
 </div>
 <div id="crowi-modals">
   {% include '../widget/page_modals.html' %}
-    {% include '../modal/what_is_portal.html' %}
-  {% include '../modal/unportalize.html' %}
 </div>
 {% endblock %}

+ 0 - 2
src/server/views/layout-kibela/user_page.html

@@ -51,13 +51,11 @@
 
   </div>
 
-  {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
   <div class="row page-list mt-5 d-edit-none">
     <div class="col-12">
       {% include '../widget/page_list_and_timeline_kibela.html' %}
     </div>
   </div>
-  {% endif %}
 
 {% endblock %}
 

+ 0 - 5
src/server/views/layout-kibela/widget/header.html

@@ -1,7 +1,2 @@
 <div id="grw-subnav" class="grw-subnav" data-is-forbidden-page="{{ forbidden }}"></div>
 
-{% if not page and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
-  {% if '/' === path.slice(-1) %}
-    {% include '../../widget/create_portal.html' %}
-  {% endif %}
-{% endif %}

+ 2 - 2
src/server/views/modal/create_page.html

@@ -62,10 +62,10 @@
                   </a>
                   <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                     <button class="dropdown-item" type="button" data-template-type="children">
-                      {{ t('template.children.label') }} (_template) <small class="text-muted">- {{ t('template.children.desc') }}</small>
+                      {{ t('template.children.label') }} (_template)<br class="d-block d-md-none" /><small class="text-muted text-wrap">- {{ t('template.children.desc') }}</small>
                     </button>
                     <button class="dropdown-item" type="button" data-template-type="decentants">
-                      {{ t('template.decendants.label') }} (__template) <small class="text-muted">- {{ t('template.decendants.desc') }}</small>
+                      {{ t('template.decendants.label') }} (__template) <br class="d-block d-md-none" /><small class="text-muted">- {{ t('template.decendants.desc') }}</small>
                     </button>
                   </div>
                 </div>

+ 0 - 50
src/server/views/modal/unportalize.html

@@ -1,50 +0,0 @@
-{% if isTopPage() %}
-  {% set unportalizedPath = '/top-' + Date.now() %}
-{% else %}
-  {% set unportalizedPath = page.path|replace('(\/)$', '') %}
-{% endif %}
-  <div class="modal" id="unportalize">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-      <form role="form" id="unportalize-form" onsubmit="return false;">
-
-        <div class="modal-header bg-warning">
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">ポータル化を解除する</div>
-        </div>
-        <div class="modal-body">
-          <ul>
-           <li>このポータル化を解除し、通常のページに戻します。</li>
-          </ul>
-            <div class="form-group">
-              <p>
-                <label for="">このページ</label><br><code>{{ page.path }}</code>
-              </p>
-              <p>
-                <label for="">解除後のページ</label><br><code>{{ unportalizedPath }}</code>
-              </p>
-              {% if isTopPage() %}
-              <p class="alert alert-info">
-              このポータルはトップページのポータルのため、特別なページに移動します。
-              </p>
-              {% endif %}
-            </div>
-        </div>
-        <div class="modal-footer">
-          <div class="d-flex justify-content-between">
-            {% include '../widget/modal/page-api-error-messages.html' %}
-            <div>
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <input type="hidden" name="path" value="{{ page.path }}">
-              <input type="hidden" name="new_path" value="{{ unportalizedPath }}">
-              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-              <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-              <button type="submit" class="btn btn-warning">Unportalize</button>
-            </div>
-        </div>
-
-      </form>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-  </div><!-- /.modal -->

+ 0 - 81
src/server/views/modal/what_is_portal.html

@@ -1,81 +0,0 @@
-<div class="modal" id="help-portal">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <div class="modal-title">What is Portal?</div>
-      </div>
-
-      <div class="modal-body">
-        <h4>Portal とは</h4>
-        <br>
-        <ul>
-          <li>すべての、スラッシュ <code>/</code> で終わるページは、その階層の一覧ページとなります。</li>
-          <li>Portal 機能を使うと、その一覧ページに対して、任意の編集コンテンツを配置することができるようになります (つまり、一般的なページと同様に、編集したコンテンツを作成でき、その内容は常にページ一覧の上部に表示されるようになります)</li>
-        </ul>
-        <br>
-
-        <hr>
-
-        <h4>想定される使われ方</h4>
-        <br>
-        <p>
-        例えば、以下のようなページの階層があったとします。
-        </p>
-        <ul>
-          <li><code>/projects</code>
-            <ul>
-              <li><code>/projects/homepage-renewal</code>
-                <ul>
-                  <li><code>/projects/homepage-renewal/...</code></li>
-                </ul>
-              </li>
-              <li><code>/projects/...</code></li>
-            </ul>
-          </li>
-        </ul>
-
-        <p>
-        こういったケースでは、<code>/projects/homepage-renewal</code> には homepage-renewal プロジェクトについてのイントロや各ページへのリンク、関係者の紹介など、homepage-renewal に関する情報を掲載しておきたいと思うはずです。
-        </p>
-        <p>
-        Poral機能を使うと、こうしたときに、<code>/projects/homepage-renewal/</code> この <strong>"一覧ページ" を、ページ化することができ、そこに、通常のページと同じように Markdown で編集したコンテンツを配置することができるようになります</strong>。
-        </p>
-
-        <p>
-        まさにそのプロジェクトのポータルページを用意したい場合などに活用してください。
-        </p>
-
-        </div>
-
-      </div>
-
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->
-
-<div class="modal fade portal-warning-modal" id="portal-warning-modal">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <div class="modal-title">ポータルに関するヒント</div>
-      </div>
-      <div class="modal-body">
-
-        <strong>Warning!</strong><br>
-
-        <p>既に <strong><a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a></strong> のページが存在します。</p>
-
-        <p>
-          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> をポータル化するには、
-          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> に移動し、「ページを移動」させてください。<br>
-          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> とは別に、このページ(<code>{{ path }}</code>)にポータルを作成する場合、このまま編集を続けて作成してください。
-        </p>
-
-      </div>
-    </div>
-  </div>
-</div>

+ 0 - 4
src/server/views/widget/create_portal.html

@@ -1,4 +0,0 @@
-<div class="portal-form-button d-edit-none">
-  <a class="btn btn-primary" id="create-portal-button" href="#edit" data-toggle="tab" {% if not user %}disabled{% endif %}>Create Portal</a>
-  <p class="form-text text-muted"><a href="#" data-target="#help-portal" data-toggle="modal"><i class="icon-question"></i> What is Portal?</a></p>
-</div>

+ 2 - 0
src/server/views/widget/not_found_tabs.html

@@ -14,5 +14,7 @@
       <i class="icon-note"></i> {{ t('Create') }}
     </a>
   </li>
+
+  <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
   {% endif %}
 </ul>

+ 0 - 3
src/server/views/widget/page_content.html

@@ -45,9 +45,6 @@
         </div>
         <div id="page" class="mt-4"></div>
       </div>
-    {% elseif 'crowi' === getConfig('crowi', 'customize:behavior') %}
-      <div class="tab-pane active" id="cancel-creating-portal">
-      </div>
     {% endif %}
 
     {% if !isTrashPage() %}

+ 2 - 2
src/server/views/widget/page_list.html

@@ -14,8 +14,8 @@
     data-path="{{ page.path }}">{{ decodeURIComponent(page.path) }}
   </a>
   <span class="page-list-meta">
-    {% if page.isPortal() %}
-      <span class="badge badge-info">PORTAL</span>
+    {% if page.isTopPage() %}
+      <span class="badge badge-info">TOP</span>
     {% endif  %}
 
     {% if page.isTemplate() %}

+ 3 - 13
src/server/views/widget/page_tabs.html

@@ -37,10 +37,7 @@
     </li>
     {% endif %}
 
-    <div class="grw-revision-path-for-edit d-none ml-2">
-      <h4 id="revision-path" class="mb-0"></h4>
-      <div id="tag-label"></div>
-    </div>
+    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
   {% endif %}
 
   {#
@@ -51,7 +48,7 @@
   <div class="mr-auto"></div>
 
   <!-- presentation -->
-  {% if not page.isPortal() %}
+  {% if not page.isTopPage() %}
     <li class="nav-item">
       <a href="?presentation=1" class="nav-link toggle-presentation">
         <i class="icon-film"></i><span class="d-none d-md-inline"> {{ t('Presentation Mode') }}</span>
@@ -68,7 +65,7 @@
 
   <!-- icon-options-vertical -->
   {% if !isTrashPage() %}
-    {% if page.isPortal() %}
+    {% if page.isTopPage() %}
     <li class="nav-item dropdown">
       <a
         {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" href="#" data-toggle="dropdown" {% endif %}
@@ -81,13 +78,6 @@
       </a>
       <div class="dropdown-menu dropdown-menu-right">
         <a class="dropdown-item" href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
-        {% if ('/' !== path) %}
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a>
-          {% if isDeletablePage() %}
-            <a class="dropdown-item" href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a>
-          {% endif %}
-        {% endif %}
       </div>
     </li>
     {% else %}

+ 29 - 30
src/server/views/widget/page_tabs_kibela.html

@@ -11,30 +11,33 @@
   </li>
 
   {% if !isTrashPage() %}
-  <li class="nav-item nav-tab-edit">
-    <a
-      {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %}
-        class="nav-link edit-button edit-button-disabled"
-        data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-      {% endif %}
-    >
-      <i class="icon-note"></i> {{ t('Edit') }}
-    </a>
-  </li>
-  {% if isHackmdSetup() %}
-  <li class="nav-item nav-tab-hackmd">
-    <a
-      {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %}
-        class="nav-link edit-button edit-button-disabled"
-        data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-      {% endif %}
-    >
-      <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
-    </a>
-  </li>
-  {% endif %}
+    <li class="nav-item nav-tab-edit">
+      <a
+        {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
+        {% if not user %}
+          class="nav-link edit-button edit-button-disabled"
+          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
+        {% endif %}
+      >
+        <i class="icon-note"></i> {{ t('Edit') }}
+      </a>
+    </li>
+    {% if isHackmdSetup() %}
+    <li class="nav-item nav-tab-hackmd">
+      <a
+        {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
+        {% if not user %}
+          class="nav-link edit-button edit-button-disabled"
+          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
+        {% endif %}
+      >
+        <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
+      </a>
+    </li>
+    {% endif %}
+
+    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+
   {% endif %}
 
   {#
@@ -43,7 +46,7 @@
   {# to place right side #}
   <div class="mr-auto"></div>
 
-  {% if not page.isPortal() %}
+  {% if not page.isTopPage() %}
   <li class="nav-item">
     <a href="?presentation=1" class="nav-link toggle-presentation">
       <i class="icon-film"></i><span class="d-none d-sm-inline"> {{ t('Presentation Mode') }}</span>
@@ -58,7 +61,7 @@
   </li>
 
   {% if !isTrashPage() %}
-    {% if page.isPortal() %}
+    {% if page.isTopPage() %}
     <li class="nav-item dropdown">
       <a
         {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
@@ -73,10 +76,6 @@
         <li class="dropdown-item">
           <a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
         </li>
-        {% if ('/' !== path) %}
-        <li class="dropdown-divider"></li>
-        <li class="dropdown-item"><a href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a></li>
-        {% endif %}
       </ul>
     </li>
     {% else %}

+ 15 - 0
yarn.lock

@@ -688,6 +688,14 @@
     "@babel/helper-plugin-utils" "^7.0.0"
     "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
 
+"@babel/plugin-proposal-optional-chaining@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+  integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
 "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78"
@@ -739,6 +747,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
+"@babel/plugin-syntax-optional-chaining@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
 "@babel/plugin-transform-arrow-functions@^7.2.0":
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"