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

Merge pull request #4939 from weseek/master

Release v4.5.4
Yuki Takei 4 лет назад
Родитель
Сommit
844fa7edc8
82 измененных файлов с 1217 добавлено и 1019 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 1 1
      packages/app/bin/download-cdn-resources.ts
  4. 0 2
      packages/app/config/webpack.common.js
  5. 0 1
      packages/app/config/webpack.dev.dll.js
  6. 12 13
      packages/app/package.json
  7. 1 0
      packages/app/resource/locales/en_US/translation.json
  8. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  9. 1 0
      packages/app/resource/locales/zh_CN/translation.json
  10. 1 1
      packages/app/src/client/app.jsx
  11. 3 0
      packages/app/src/client/interfaces/focusable.ts
  12. 13 0
      packages/app/src/client/interfaces/react-bootstrap-typeahead.ts
  13. 12 6
      packages/app/src/client/services/ContextExtractor.tsx
  14. 1 0
      packages/app/src/client/services/PageContainer.js
  15. 47 0
      packages/app/src/client/util/codemirror/drawio-fold.ext.js
  16. 1 1
      packages/app/src/client/util/interceptor/detach-code-blocks.js
  17. 17 2
      packages/app/src/client/util/interceptor/drawio-interceptor.js
  18. 2 0
      packages/app/src/components/Hotkeys/HotkeysManager.jsx
  19. 34 0
      packages/app/src/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx
  20. 25 3
      packages/app/src/components/Navbar/AuthorInfo.jsx
  21. 0 108
      packages/app/src/components/Navbar/GlobalSearch.jsx
  22. 120 0
      packages/app/src/components/Navbar/GlobalSearch.tsx
  23. 4 1
      packages/app/src/components/Navbar/GrowiSubNavigation.jsx
  24. 3 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  25. 7 2
      packages/app/src/components/PageContentFooter.jsx
  26. 1 1
      packages/app/src/components/PageEditor.jsx
  27. 27 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  28. 0 1
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  29. 16 0
      packages/app/src/components/PageEditor/MarkdownDrawioUtil.js
  30. 1 1
      packages/app/src/components/PageEditor/MarkdownTableInterceptor.js
  31. 36 0
      packages/app/src/components/PageEditor/OptionsSelector.jsx
  32. 1 1
      packages/app/src/components/PageEditor/PreventMarkdownListInterceptor.js
  33. 0 1
      packages/app/src/components/PagePathAutoComplete.jsx
  34. 0 177
      packages/app/src/components/SearchForm.jsx
  35. 152 0
      packages/app/src/components/SearchForm.tsx
  36. 4 1
      packages/app/src/components/SearchPage/SearchPageForm.jsx
  37. 0 274
      packages/app/src/components/SearchTypeahead.jsx
  38. 242 0
      packages/app/src/components/SearchTypeahead.tsx
  39. 1 1
      packages/app/src/server/models/GlobalNotificationSetting/index.js
  40. 1 1
      packages/app/src/server/models/page.js
  41. 1 1
      packages/app/src/server/routes/apiv3/app-settings.js
  42. 1 1
      packages/app/src/server/routes/apiv3/pages.js
  43. 1 1
      packages/app/src/server/routes/page.js
  44. 1 1
      packages/app/src/server/service/config-loader.ts
  45. 1 1
      packages/app/src/server/service/slack-command-handler/create-page-service.js
  46. 1 1
      packages/app/src/server/util/middlewares.js
  47. 1 1
      packages/app/src/server/util/swigFunctions.js
  48. 5 1
      packages/app/src/server/views/modal/shortcuts.html
  49. 2 2
      packages/app/src/server/views/widget/page_content.html
  50. 1 1
      packages/app/src/services/cdn-resources-service.js
  51. 4 4
      packages/app/src/stores/context.tsx
  52. 9 0
      packages/app/src/stores/ui.tsx
  53. 11 10
      packages/app/src/styles/_search.scss
  54. 2 1
      packages/app/src/styles/_sidebar.scss
  55. 1 1
      packages/codemirror-textlint/package.json
  56. 0 27
      packages/core/README.md
  57. 1 1
      packages/core/package.json
  58. 11 14
      packages/core/src/index.js
  59. 2 4
      packages/core/src/plugin/service/tag-cache-manager.js
  60. 1 3
      packages/core/src/service/localstorage-manager.js
  61. 1 3
      packages/core/src/utils/basic-interceptor.js
  62. 1 1
      packages/plugin-attachment-refs/package.json
  63. 1 1
      packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  64. 1 1
      packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js
  65. 1 1
      packages/plugin-attachment-refs/src/client/js/util/RefsContext.js
  66. 1 1
      packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js
  67. 1 1
      packages/plugin-attachment-refs/src/server/routes/refs.js
  68. 1 1
      packages/plugin-lsx/package.json
  69. 1 1
      packages/plugin-lsx/src/client/js/components/Lsx.jsx
  70. 1 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx
  71. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js
  72. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js
  73. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js
  74. 1 1
      packages/plugin-lsx/src/client/js/util/LsxContext.js
  75. 1 18
      packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js
  76. 1 1
      packages/plugin-lsx/src/server/routes/lsx.js
  77. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  78. 1 1
      packages/slack/package.json
  79. 2 2
      packages/slackbot-proxy/package.json
  80. 0 27
      packages/ui/README.md
  81. 1 1
      packages/ui/package.json
  82. 349 272
      yarn.lock

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 1 - 1
packages/app/bin/download-cdn-resources.ts

@@ -3,7 +3,7 @@
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 
 import CdnResourcesDownloader from './cdn/cdn-resources-downloader';
 import loggerFactory from '../src/utils/logger';

+ 0 - 2
packages/app/config/webpack.common.js

@@ -83,8 +83,6 @@ module.exports = (options) => {
           exclude: {
             test: /node_modules/,
             exclude: [ // include as a result
-              { test: /node_modules\/growi-plugin-/ },
-              /node_modules\/growi-commons/,
               /node_modules\/codemirror/,
             ],
           },

+ 0 - 1
packages/app/config/webpack.dev.dll.js

@@ -17,7 +17,6 @@ module.exports = {
       'diff2html',
       'debug',
       'entities',
-      'growi-commons',
       'i18next', 'i18next-browser-languagedetector',
       'jquery-slimscroll',
       'lodash', 'pako',

+ 12 - 13
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.5.3",
+  "version": "4.5.4-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -58,11 +58,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.5.3",
-    "@growi/plugin-attachment-refs": "^4.5.3",
-    "@growi/plugin-lsx": "^4.5.3",
-    "@growi/plugin-pukiwiki-like-linker": "^4.5.3",
-    "@growi/slack": "^4.5.3",
+    "@growi/codemirror-textlint": "^4.5.4-RC.0",
+    "@growi/plugin-attachment-refs": "^4.5.4-RC.0",
+    "@growi/plugin-lsx": "^4.5.4-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.5.4-RC.0",
+    "@growi/slack": "^4.5.4-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -99,12 +99,11 @@
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
     "http-errors": "~1.8.0",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
-    "i18next-node-fs-backend": "^2.1.0",
+    "i18next-node-fs-backend": "^2.1.3",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
@@ -147,7 +146,7 @@
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^13.6.0",
-    "ws": "^7.4.6",
+    "ws": "^8.3.0",
     "xss": "^1.0.6"
   },
   "// comments for defDependencies": {
@@ -156,8 +155,8 @@
     "ts-loader": "v9 is not compatible with webpack@5"
   },
   "devDependencies": {
-    "@alienfast/i18next-loader": "^1.0.16",
-    "@growi/ui": "^4.5.3",
+    "@alienfast/i18next-loader": "^1.1.4",
+    "@growi/ui": "^4.5.4-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -176,7 +175,7 @@
     "css-loader": "^3.0.0",
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
-    "eazy-logger": "^3.0.2",
+    "eazy-logger": "^3.1.0",
     "file-loader": "^5.0.2",
     "handsontable": "=6.2.2",
     "hard-source-webpack-plugin": "^0.13.1",
@@ -244,7 +243,7 @@
     "unstated": "^2.1.1",
     "webpack": "^4.39.3",
     "webpack-assets-manifest": "^3.1.1",
-    "webpack-bundle-analyzer": "^3.0.2",
+    "webpack-bundle-analyzer": "^3.9.0",
     "webpack-cli": "^3.3.7",
     "webpack-merge": "^4.2.2"
   }

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

@@ -439,6 +439,7 @@
       "Open/Close shortcut help": "Open/Close<br>shortcut help",
       "Edit Page": "Edit Page",
       "Create Page": "Create Page",
+      "Search": "Search",
       "Show Contributors": "Show Contributors",
       "MirrorMode": "Mirror Mode",
       "Konami Code": "Konami Code",

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

@@ -439,6 +439,7 @@
       "Open/Close shortcut help": "ショートカットヘルプ<br>の表示/非表示",
       "Edit Page": "ページ編集",
       "Create Page": "ページ作成",
+      "Search": "検索",
       "Show Contributors": "コントリビューター<br>を表示",
       "MirrorMode": "ミラーモード",
       "Konami Code": "コナミコマンド",

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

@@ -418,6 +418,7 @@
 			"Open/Close shortcut help": "打开/关闭快捷方式帮助",
 			"Edit Page": "编辑页面",
 			"Create Page": "创建页面",
+      "Search": "搜索",
 			"Show Contributors": "显示参与者",
 			"Konami Code": "Konami Code",
 			"konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"

+ 1 - 1
packages/app/src/client/app.jsx

@@ -98,7 +98,7 @@ Object.assign(componentMappings, {
   'not-found-page': <NotFoundPage />,
   'not-found-alert': <NotFoundAlert
     isGuestUserMode={appContainer.isGuestUser}
-    isHidden={pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
+    isHidden={pageContainer.state.pageId != null ? (pageContainer.state.isNotCreatable ?? pageContainer.state.isTrashPage) : false} // !!DO NOT MOVE THIS!! https://github.com/weseek/growi/pull/4899
   />,
 
   'forbidden-page': <ForbiddenPage />,

+ 3 - 0
packages/app/src/client/interfaces/focusable.ts

@@ -0,0 +1,3 @@
+export interface IFocusable {
+  focus: () => void,
+}

+ 13 - 0
packages/app/src/client/interfaces/react-bootstrap-typeahead.ts

@@ -0,0 +1,13 @@
+// https://github.com/ericgio/react-bootstrap-typeahead/blob/3.x/docs/Props.md
+export type TypeaheadProps = {
+  dropup?: boolean,
+  emptyLabel?: string,
+  placeholder?: string,
+  autoFocus?: boolean,
+
+  onChange?: (data: unknown[]) => void,
+  onBlur?: () => void,
+  onFocus?: () => void,
+  onInputChange?: (text: string) => void,
+  onKeyDown?: (input: string) => void,
+};

+ 12 - 6
packages/app/src/client/services/ContextExtractor.tsx

@@ -2,10 +2,10 @@ import React, { FC, useEffect, useState } from 'react';
 import { pagePathUtils } from '@growi/core';
 
 import {
-  useCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
+  useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   usePageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
-  useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
+  useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
   useSlackChannels,
 } from '../../stores/context';
 import {
@@ -39,8 +39,14 @@ const ContextExtractorOnce: FC = () => {
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
   const pageId = mainContent?.getAttribute('data-page-id') || null;
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
-  const createdAt = mainContent?.getAttribute('data-page-created-at');
-  const updatedAt = mainContent?.getAttribute('data-page-updated-at');
+
+  // createdAt
+  const createdAtAttribute = mainContent?.getAttribute('data-page-created-at');
+  const createdAt: Date | null = (createdAtAttribute != null) ? new Date(createdAtAttribute) : null;
+  // updatedAt
+  const updatedAtAttribute = mainContent?.getAttribute('data-page-updated-at');
+  const updatedAt: Date | null = (updatedAtAttribute != null) ? new Date(updatedAtAttribute) : null;
+
   const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
   const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull);
   const isTrashPage = _isTrashPage(path);
@@ -76,7 +82,7 @@ const ContextExtractorOnce: FC = () => {
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 
   // Page
-  useCreatedAt(createdAt);
+  useCurrentCreatedAt(createdAt);
   useDeleteUsername(deleteUsername);
   useDeletedAt(deletedAt);
   useHasChildren(hasChildren);
@@ -99,7 +105,7 @@ const ContextExtractorOnce: FC = () => {
   useShareLinkId(shareLinkId);
   useShareLinksNumber(shareLinksNumber);
   useTemplateTagData(templateTagData);
-  useUpdatedAt(updatedAt);
+  useCurrentUpdatedAt(updatedAt);
   useCreator(creator);
   useRevisionAuthor(revisionAuthor);
 

+ 1 - 0
packages/app/src/client/services/PageContainer.js

@@ -65,6 +65,7 @@ export default class PageContainer extends Container {
       sumOfLikers: 0,
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
+      // please use useCurrentUpdatedAt instead
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
 

+ 47 - 0
packages/app/src/client/util/codemirror/drawio-fold.ext.js

@@ -0,0 +1,47 @@
+/* eslint-disable */
+
+import mdu from '../../../components/PageEditor/MarkdownDrawioUtil.js';
+
+(function(mod) {
+  mod(require("codemirror"));
+})(function(CodeMirror) {
+  "use strict"
+
+  CodeMirror.registerGlobalHelper('fold', 'drawio', function (mode, cm) {
+    return true;
+  }, function(cm, start) {
+    function isBeginningOfDrawio(lineNo) {
+      let line = cm.getLine(lineNo);
+      let match = mdu.lineBeginPartOfDrawioRE.exec(line);
+      if (match) {
+        return true;
+      }
+      return false;
+    }
+    function isEndOfDrawio(lineNo) {
+      let line = cm.getLine(lineNo);
+      let match = mdu.lineEndPartOfDrawioRE.exec(line);
+      if (match) {
+        return true;
+      }
+      return false;
+    }
+
+    let drawio = isBeginningOfDrawio(start.line);
+    if (drawio === false) { return; }
+
+    let lastLine = cm.lastLine();
+    let end = start.line;
+    while(end < lastLine) {
+      end += 1;
+      if (isEndOfDrawio(end)) {
+        break;
+      }
+    }
+
+    return {
+      from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
+      to: CodeMirror.Pos(end, cm.getLine(end).length)
+    };
+  });
+});

+ 1 - 1
packages/app/src/client/util/interceptor/detach-code-blocks.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
 

+ 17 - 2
packages/app/src/client/util/interceptor/drawio-interceptor.js

@@ -2,7 +2,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import Drawio from '~/components/Drawio';
 
@@ -103,11 +103,18 @@ export class DrawioInterceptor extends BasicInterceptor {
    */
   drawioPostRender(contextName, context) {
     const isPreview = (contextName === 'postRenderPreviewHtml');
+    const editorContainer = this.appContainer.getContainer('EditorContainer');
+    const renderDrawioInRealtime = editorContainer.state.previewOptions.renderDrawioInRealtime;
 
     Object.keys(context.DrawioMap).forEach((domId) => {
       const elem = document.getElementById(domId);
       if (elem) {
-        this.renderReactDOM(context.DrawioMap[domId], elem, isPreview);
+        if (isPreview && !renderDrawioInRealtime) {
+          this.renderDisabledDrawioReactDOM(context.DrawioMap[domId], elem, isPreview);
+        }
+        else {
+          this.renderReactDOM(context.DrawioMap[domId], elem, isPreview);
+        }
       }
     });
   }
@@ -129,6 +136,14 @@ export class DrawioInterceptor extends BasicInterceptor {
     );
   }
 
+  renderDisabledDrawioReactDOM(drawioMapEntry, elem, isPreview) {
+    ReactDOM.render(
+      // eslint-disable-next-line react/jsx-filename-extension
+      <div className="alert alert-light text-dark">Rendering of draw.io is disabled.</div>,
+      elem,
+    );
+  }
+
   /**
    * @inheritdoc
    */

+ 2 - 0
packages/app/src/components/Hotkeys/HotkeysManager.jsx

@@ -7,6 +7,7 @@ import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
 import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
 import CreatePage from './Subscribers/CreatePage';
 import EditPage from './Subscribers/EditPage';
+import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
 
 // define supported components list
 const SUPPORTED_COMPONENTS = [
@@ -15,6 +16,7 @@ const SUPPORTED_COMPONENTS = [
   ShowShortcutsModal,
   CreatePage,
   EditPage,
+  FocusToGlobalSearch,
 ];
 
 const KEY_SET = new Set();

+ 34 - 0
packages/app/src/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx

@@ -0,0 +1,34 @@
+import { FC, useEffect } from 'react';
+
+import { useIsEditable } from '~/stores/context';
+import { useGlobalSearchFormRef } from '~/stores/ui';
+
+const FocusToGlobalSearch = (props) => {
+  const { data: isEditable } = useIsEditable();
+  const { data: globalSearchFormRef } = useGlobalSearchFormRef();
+
+  // setup effect
+  useEffect(() => {
+    if (!isEditable) {
+      return;
+    }
+
+    // ignore when dom that has 'modal in' classes exists
+    if (document.getElementsByClassName('modal in').length > 0) {
+      return;
+    }
+
+    globalSearchFormRef.current.focus();
+
+    // remove this
+    props.onDeleteRender();
+  }, [globalSearchFormRef, isEditable, props]);
+
+  return null;
+};
+
+FocusToGlobalSearch.getHotkeyStrokes = () => {
+  return [['/']];
+};
+
+export default FocusToGlobalSearch;

+ 25 - 3
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -16,6 +16,9 @@ const AuthorInfo = (props) => {
   const infoLabelForSubNav = mode === 'create'
     ? 'Created by'
     : 'Updated by';
+  const nullinfoLabelForFooter = mode === 'create'
+    ? 'Created by'
+    : 'Updated by';
   const infoLabelForFooter = mode === 'create'
     ? 'Created at'
     : 'Last revision posted at';
@@ -24,9 +27,26 @@ const AuthorInfo = (props) => {
     : <i>Unknown</i>;
 
   if (locate === 'footer') {
-    return <p>{infoLabelForFooter} {format(new Date(date), formatType)} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+    try {
+      return <p>{infoLabelForFooter} {format(new Date(date), formatType)} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+    }
+    catch (err) {
+      if (err instanceof RangeError) {
+        return <p>{nullinfoLabelForFooter} <UserPicture user={user} size="sm" /> {userLabel}</p>;
+      }
+      return;
+    }
   }
 
+  const renderParsedDate = () => {
+    try {
+      return format(new Date(date), formatType);
+    }
+    catch (err) {
+      return '';
+    }
+  };
+
   return (
     <div className="d-flex align-items-center">
       <div className="mr-2">
@@ -34,14 +54,16 @@ const AuthorInfo = (props) => {
       </div>
       <div>
         <div>{infoLabelForSubNav} {userLabel}</div>
-        <div className="text-muted text-date">{format(new Date(date), formatType)}</div>
+        <div className="text-muted text-date">
+          {renderParsedDate()}
+        </div>
       </div>
     </div>
   );
 };
 
 AuthorInfo.propTypes = {
-  date: PropTypes.string.isRequired,
+  date: PropTypes.instanceOf(Date),
   user: PropTypes.object,
   mode: PropTypes.oneOf(['create', 'update']),
   locate: PropTypes.oneOf(['subnav', 'footer']),

+ 0 - 108
packages/app/src/components/Navbar/GlobalSearch.jsx

@@ -1,108 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import SearchForm from '../SearchForm';
-
-
-class GlobalSearch extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const isSearchScopeChildrenAsDefault = this.props.appContainer.getConfig().isSearchScopeChildrenAsDefault;
-
-    this.state = {
-      text: '',
-      isScopeChildren: isSearchScopeChildrenAsDefault,
-    };
-
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onClickAllPages = this.onClickAllPages.bind(this);
-    this.onClickChildren = this.onClickChildren.bind(this);
-    this.search = this.search.bind(this);
-  }
-
-  onInputChange(text) {
-    this.setState({ text });
-  }
-
-  onClickAllPages() {
-    this.setState({ isScopeChildren: false });
-  }
-
-  onClickChildren() {
-    this.setState({ isScopeChildren: true });
-  }
-
-  search() {
-    const url = new URL(window.location.href);
-    url.pathname = '/_search';
-
-    // construct search query
-    let q = this.state.text;
-    if (this.state.isScopeChildren) {
-      q += ` prefix:${window.location.pathname}`;
-    }
-    url.searchParams.append('q', q);
-
-    window.location.href = url.href;
-  }
-
-  render() {
-    const { t, appContainer, dropup } = this.props;
-    const scopeLabel = this.state.isScopeChildren
-      ? t('header_search_box.label.This tree')
-      : t('header_search_box.label.All pages');
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-    return (
-      <div className={`form-group mb-0 d-print-none ${isReachable ? '' : 'has-error'}`}>
-        <div className="input-group flex-nowrap">
-          <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
-            <button className="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true">
-              {scopeLabel}
-            </button>
-            <div className="dropdown-menu">
-              <button className="dropdown-item" type="button" onClick={this.onClickAllPages}>{ t('header_search_box.item_label.All pages') }</button>
-              <button className="dropdown-item" type="button" onClick={this.onClickChildren}>{ t('header_search_box.item_label.This tree') }</button>
-            </div>
-          </div>
-          <SearchForm
-            t={this.props.t}
-            crowi={this.props.appContainer}
-            onInputChange={this.onInputChange}
-            onSubmit={this.search}
-            placeholder="Search ..."
-            dropup={dropup}
-          />
-          <div className="btn-group-submit-search">
-            <span className="btn-link text-decoration-none" onClick={this.search}>
-              <i className="icon-magnifier"></i>
-            </span>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-GlobalSearch.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  dropup: PropTypes.bool,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const GlobalSearchWrapper = withUnstatedContainers(GlobalSearch, [AppContainer]);
-
-export default withTranslation()(GlobalSearchWrapper);

+ 120 - 0
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -0,0 +1,120 @@
+import React, {
+  FC, useState, useCallback, useRef,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import AppContainer from '~/client/services/AppContainer';
+import { IPage } from '~/interfaces/page';
+import { IFocusable } from '~/client/interfaces/focusable';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import SearchForm from '../SearchForm';
+import { useGlobalSearchFormRef } from '~/stores/ui';
+
+
+type Props = {
+  appContainer: AppContainer,
+
+  dropup?: boolean,
+}
+
+const GlobalSearch: FC<Props> = (props: Props) => {
+  const { appContainer, dropup } = props;
+  const { t } = useTranslation();
+
+  const globalSearchFormRef = useRef<IFocusable>(null);
+
+  useGlobalSearchFormRef(globalSearchFormRef);
+
+  const [text, setText] = useState('');
+  const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
+  const [isFocused, setFocused] = useState<boolean>(false);
+
+  const gotoPage = useCallback((data: unknown[]) => {
+    const page = data[0] as IPage; // should be single page selected
+
+    // navigate to page
+    if (page != null) {
+      window.location.href = page.path;
+    }
+  }, []);
+
+  const search = useCallback(() => {
+    const url = new URL(window.location.href);
+    url.pathname = '/_search';
+
+    // construct search query
+    let q = text;
+    if (isScopeChildren) {
+      q += ` prefix:${window.location.pathname}`;
+    }
+    url.searchParams.append('q', q);
+
+    window.location.href = url.href;
+  }, [isScopeChildren, text]);
+
+  const scopeLabel = isScopeChildren
+    ? t('header_search_box.label.This tree')
+    : t('header_search_box.label.All pages');
+
+  const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
+
+  const isIndicatorShown = !isFocused && (text.length === 0);
+
+  return (
+    <div className={`form-group mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
+      <div className="input-group flex-nowrap">
+        <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
+          <button className="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true">
+            {scopeLabel}
+          </button>
+          <div className="dropdown-menu">
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setScopeChildren(false);
+                globalSearchFormRef.current?.focus();
+              }}
+            >
+              { t('header_search_box.item_label.All pages') }
+            </button>
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setScopeChildren(true);
+                globalSearchFormRef.current?.focus();
+              }}
+            >
+              { t('header_search_box.item_label.This tree') }
+            </button>
+          </div>
+        </div>
+        <SearchForm
+          ref={globalSearchFormRef}
+          isSearchServiceReachable={isSearchServiceReachable}
+          dropup={dropup}
+          onChange={gotoPage}
+          onBlur={() => setFocused(false)}
+          onFocus={() => setFocused(true)}
+          onInputChange={text => setText(text)}
+          onSubmit={search}
+        />
+        { isIndicatorShown && (
+          <span className="grw-shortcut-key-indicator">
+            <code className="bg-transparent text-muted">/</code>
+          </span>
+        ) }
+      </div>
+    </div>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GlobalSearchWrapper = withUnstatedContainers(GlobalSearch, [AppContainer]);
+
+export default GlobalSearchWrapper;

+ 4 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -11,6 +11,7 @@ import PageContainer from '~/client/services/PageContainer';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 import CopyDropdown from '../Page/CopyDropdown';
 import TagLabels from '../Page/TagLabels';
@@ -70,12 +71,14 @@ const GrowiSubNavigation = (props) => {
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+  const { data: createdAt } = useCurrentCreatedAt();
+  const { data: updatedAt } = useCurrentUpdatedAt();
 
   const {
     appContainer, pageContainer, isCompactMode,
   } = props;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isPageExist,
+    pageId, path, creator, revisionAuthor, isPageExist,
   } = pageContainer.state;
 
   const { isGuestUser } = appContainer;

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

@@ -7,6 +7,7 @@ import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { useCurrentUpdatedAt } from '~/stores/context';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';
@@ -15,8 +16,9 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const {
-    path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    path, isDeleted, lastUpdateUsername, deletedUserName, deletedAt, isAbleToDeleteCompletely,
   } = pageContainer.state;
+  const { data: updatedAt } = useCurrentUpdatedAt();
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
   const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);

+ 7 - 2
packages/app/src/components/PageContentFooter.jsx

@@ -6,14 +6,19 @@ import AuthorInfo from './Navbar/AuthorInfo';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
-import { usePath } from '~/stores/context';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 const PageContentFooter = (props) => {
   const { pageContainer } = props;
+  const { data: createdAt } = useCurrentCreatedAt();
+  const { data: updatedAt } = useCurrentUpdatedAt();
+
+
   const {
-    createdAt, creator, updatedAt, revisionAuthor,
+    creator, revisionAuthor,
   } = pageContainer.state;
 
+
   return (
     <div className="page-content-footer py-4 d-edit-none d-print-none">
       <div className="grw-container-convertible">

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

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import detectIndent from 'detect-indent';
 
 import { throttle, debounce } from 'throttle-debounce';
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 import AppContainer from '~/client/services/AppContainer';

+ 27 - 1
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -66,6 +66,7 @@ require('codemirror/addon/display/placeholder');
 require('codemirror/addon/lint/lint');
 require('codemirror/addon/lint/lint.css');
 require('~/client/util/codemirror/autorefresh.ext');
+require('~/client/util/codemirror/drawio-fold.ext');
 require('~/client/util/codemirror/gfm-growi.mode');
 // import modes to highlight
 require('codemirror/mode/clike/clike');
@@ -149,6 +150,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.showLinkEditHandler = this.showLinkEditHandler.bind(this);
     this.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
     this.showDrawioHandler = this.showDrawioHandler.bind(this);
+
+    this.foldDrawioSection = this.foldDrawioSection.bind(this);
+    this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
   }
 
   init() {
@@ -185,6 +189,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
     // set keymap
     const keymapMode = this.props.editorOptions.keymapMode;
     this.setKeymapMode(keymapMode);
+
+    // fold drawio section
+    this.foldDrawioSection();
   }
 
   componentWillReceiveProps(nextProps) {
@@ -195,6 +202,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
     // set keymap
     const keymapMode = nextProps.editorOptions.keymapMode;
     this.setKeymapMode(keymapMode);
+
+    // fold drawio section
+    this.foldDrawioSection();
   }
 
   async initializeTextlint() {
@@ -741,6 +751,22 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
   }
 
+  // fold draw.io section (::: drawio ~ :::)
+  foldDrawioSection() {
+    const editor = this.getCodeMirror();
+    const lineNumbers = mdu.findAllDrawioSection(editor);
+    lineNumbers.forEach((lineNumber) => {
+      editor.foldCode({ line: lineNumber, ch: 0 }, { scanUp: false }, 'fold');
+    });
+  }
+
+  onSaveForDrawio(drawioData) {
+    const range = mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioData);
+    // Fold the section after the drawio section (:::drawio) has been updated.
+    this.foldDrawioSection();
+    return range;
+  }
+
   getNavbarItems() {
     return [
       <Button
@@ -974,7 +1000,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
         />
         <DrawioModal
           ref={this.drawioModal}
-          onSave={(drawioData) => { return mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioData) }}
+          onSave={this.onSaveForDrawio}
         />
 
       </React.Fragment>

+ 0 - 1
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -292,7 +292,6 @@ class LinkEditModal extends React.PureComponent {
                 inputName="link"
                 placeholder={t('link_edit.placeholder_of_link_input')}
                 keywordOnInit={this.state.linkInputValue}
-                behaviorOfResetBtn="clear"
                 autoFocus
               />
               <div className="d-none d-sm-block input-group-append">

+ 16 - 0
packages/app/src/components/PageEditor/MarkdownDrawioUtil.js

@@ -155,6 +155,22 @@ class MarkdownDrawioUtil {
     return newMarkdown;
   }
 
+  /**
+   * return an array of the starting line numbers of the drawio sections found in markdown
+   */
+  findAllDrawioSection(editor) {
+    const lineNumbers = [];
+    // refs: https://github.com/codemirror/CodeMirror/blob/5.64.0/addon/fold/foldcode.js#L106-L111
+    for (let i = editor.firstLine(), e = editor.lastLine(); i <= e; i++) {
+      const line = editor.getLine(i);
+      const match = this.lineBeginPartOfDrawioRE.exec(line);
+      if (match) {
+        lineNumbers.push(i);
+      }
+    }
+    return lineNumbers;
+  }
+
 }
 
 // singleton pattern

+ 1 - 1
packages/app/src/components/PageEditor/MarkdownTableInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import mtu from './MarkdownTableUtil';
 import MarkdownTable from '~/client/models/MarkdownTable';

+ 36 - 0
packages/app/src/components/PageEditor/OptionsSelector.jsx

@@ -22,6 +22,7 @@ export const defaultEditorOptions = {
 
 export const defaultPreviewOptions = {
   renderMathJaxInRealtime: false,
+  renderDrawioInRealtime: true,
 };
 
 class OptionsSelector extends React.Component {
@@ -54,6 +55,7 @@ class OptionsSelector extends React.Component {
     this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
     this.onClickStyleActiveLine = this.onClickStyleActiveLine.bind(this);
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
+    this.onClickRenderDrawioInRealtime = this.onClickRenderDrawioInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.switchTextlintEnabledHandler = this.switchTextlintEnabledHandler.bind(this);
     this.confirmEnableTextlintHandler = this.confirmEnableTextlintHandler.bind(this);
@@ -108,6 +110,17 @@ class OptionsSelector extends React.Component {
     editorContainer.saveOptsToLocalStorage();
   }
 
+  onClickRenderDrawioInRealtime(event) {
+    const { editorContainer } = this.props;
+
+    const newValue = !editorContainer.state.previewOptions.renderDrawioInRealtime;
+    const newOpts = Object.assign(editorContainer.state.previewOptions, { renderDrawioInRealtime: newValue });
+    editorContainer.setState({ previewOptions: newOpts });
+
+    // save to localStorage
+    editorContainer.saveOptsToLocalStorage();
+  }
+
   onClickMarkdownTableAutoFormatting(event) {
     const { editorContainer } = this.props;
 
@@ -249,6 +262,7 @@ class OptionsSelector extends React.Component {
           <DropdownMenu>
             {this.renderActiveLineMenuItem()}
             {this.renderRealtimeMathJaxMenuItem()}
+            {this.renderRealtimeDrawioMenuItem()}
             {this.renderMarkdownTableAutoFormattingMenuItem()}
             {this.renderIsTextlintEnabledMenuItem()}
             {/* <DropdownItem divider /> */}
@@ -308,6 +322,28 @@ class OptionsSelector extends React.Component {
     );
   }
 
+  renderRealtimeDrawioMenuItem() {
+    const { editorContainer } = this.props;
+
+    const isActive = editorContainer.state.previewOptions.renderDrawioInRealtime;
+
+    const iconClasses = ['text-info'];
+    if (isActive) {
+      iconClasses.push('ti-check');
+    }
+    const iconClassName = iconClasses.join(' ');
+
+    return (
+      <DropdownItem toggle={false} onClick={this.onClickRenderDrawioInRealtime}>
+        <div className="d-flex justify-content-between">
+          <span className="icon-container"><img src="/images/icons/fx.svg" width="14px" alt="fx"></img></span>
+          <span className="menuitem-label">draw.io Rendering</span>
+          <span className="icon-container"><i className={iconClassName}></i></span>
+        </div>
+      </DropdownItem>
+    );
+  }
+
   renderMarkdownTableAutoFormattingMenuItem() {
     const { t, editorContainer } = this.props;
     // Auto-formatting was enabled before optionalizing, so we made it a disabled option(ignoreMarkdownTableAutoFormatting).

+ 1 - 1
packages/app/src/components/PageEditor/PreventMarkdownListInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import mlu from './MarkdownListUtil';
 

+ 0 - 1
packages/app/src/components/PagePathAutoComplete.jsx

@@ -41,7 +41,6 @@ const PagePathAutoComplete = (props) => {
       onChange={inputChangeHandler}
       onInputChange={props.onInputChange}
       inputName="new_path"
-      behaviorOfResetBtn="clear"
       placeholder="Input page path"
       keywordOnInit={getKeywordOnInit(initializedPath)}
       autoFocus={props.autoFocus}

+ 0 - 177
packages/app/src/components/SearchForm.jsx

@@ -1,177 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import SearchTypeahead from './SearchTypeahead';
-
-// SearchTypeahead wrapper
-class SearchForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      searchError: null,
-      isShownHelp: false,
-    };
-
-    this.onSearchError = this.onSearchError.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.onBlur = this.onBlur.bind(this);
-    this.onFocus = this.onFocus.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  onSearchError(err) {
-    this.setState({
-      searchError: err,
-    });
-  }
-
-  onChange(selected) {
-    const page = selected[0]; // should be single page selected
-
-    // navigate to page
-    if (page != null) {
-      window.location = page.path;
-    }
-  }
-
-  onBlur() {
-    this.setState({
-      isShownHelp: false,
-    });
-
-    this.getHelpElement();
-  }
-
-  onFocus() {
-    this.setState({
-      isShownHelp: true,
-    });
-  }
-
-  getHelpElement() {
-    const { t, appContainer } = this.props;
-    const { isShownHelp } = this.state;
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-
-    if (!isReachable) {
-      return (
-        <>
-          <h5 className="text-danger">Error occured on Search Service</h5>
-          Try to reconnect from management page.
-        </>
-      );
-    }
-
-    if (!isShownHelp) {
-      return <></>;
-    }
-
-    return (
-      <table className="table grw-search-table search-help m-0">
-        <caption className="text-left text-primary p-2">
-          <h5 className="h6"><i className="icon-magnifier pr-2 mb-2" />{ t('search_help.title') }</h5>
-        </caption>
-        <tbody>
-          <tr>
-            <th className="py-2">
-              <code>word1</code> <code>word2</code><br></br>
-              <small>({ t('search_help.and.syntax help') })</small>
-            </th>
-            <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2">
-              <code>&quot;This is GROWI&quot;</code><br></br>
-              <small>({ t('search_help.phrase.syntax help') })</small>
-            </th>
-            <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-keyword</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>prefix:/user/</code></th>
-            <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-prefix:/user/</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>tag:wiki</code></th>
-            <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-tag:wiki</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
-          </tr>
-        </tbody>
-      </table>
-    );
-  }
-
-  render() {
-    const { t, appContainer, dropup } = this.props;
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-    const placeholder = isReachable
-      ? 'Search ...'
-      : 'Error on Search Service';
-    const emptyLabel = (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : t('search.search page bodies');
-
-    return (
-      <SearchTypeahead
-        dropup={dropup}
-        onChange={this.onChange}
-        onSubmit={this.props.onSubmit}
-        onInputChange={this.props.onInputChange}
-        onSearchError={this.onSearchError}
-        emptyLabel={emptyLabel}
-        placeholder={placeholder}
-        helpElement={this.getHelpElement()}
-        keywordOnInit={this.props.keyword}
-        onBlur={this.onBlur}
-        onFocus={this.onFocus}
-      />
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SearchFormWrapper = withUnstatedContainers(SearchForm, [AppContainer]);
-
-SearchForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  dropup: PropTypes.bool,
-  keyword: PropTypes.string,
-  onSubmit: PropTypes.func.isRequired,
-  onInputChange: PropTypes.func,
-};
-
-SearchForm.defaultProps = {
-  onInputChange: () => {},
-};
-
-export default SearchFormWrapper;

+ 152 - 0
packages/app/src/components/SearchForm.tsx

@@ -0,0 +1,152 @@
+import React, {
+  FC, forwardRef, ForwardRefRenderFunction, useImperativeHandle,
+  useRef, useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { IFocusable } from '~/client/interfaces/focusable';
+
+import SearchTypeahead from './SearchTypeahead';
+
+
+type SearchFormHelpProps = {
+  isReachable: boolean,
+  isShownHelp: boolean,
+}
+
+const SearchFormHelp: FC<SearchFormHelpProps> = (props: SearchFormHelpProps) => {
+  const { t } = useTranslation();
+
+  const { isReachable, isShownHelp } = props;
+
+  if (!isReachable) {
+    return (
+      <>
+        <h5 className="text-danger">Error occured on Search Service</h5>
+        Try to reconnect from management page.
+      </>
+    );
+  }
+
+  if (!isShownHelp) {
+    return <></>;
+  }
+
+  return (
+    <table className="table grw-search-table search-help m-0">
+      <caption className="text-left text-primary p-2">
+        <h5 className="h6"><i className="icon-magnifier pr-2 mb-2" />{ t('search_help.title') }</h5>
+      </caption>
+      <tbody>
+        <tr>
+          <th className="py-2">
+            <code>word1</code> <code>word2</code><br></br>
+            <small>({ t('search_help.and.syntax help') })</small>
+          </th>
+          <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2">
+            <code>&quot;This is GROWI&quot;</code><br></br>
+            <small>({ t('search_help.phrase.syntax help') })</small>
+          </th>
+          <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-keyword</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>prefix:/user/</code></th>
+          <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-prefix:/user/</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>tag:wiki</code></th>
+          <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-tag:wiki</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
+        </tr>
+      </tbody>
+    </table>
+  );
+};
+
+
+type Props = {
+  isSearchServiceReachable: boolean,
+
+  dropup?: boolean,
+  keyword?: string,
+  onChange?: (data: unknown[]) => void,
+  onBlur?: () => void,
+  onFocus?: () => void,
+  onSubmit?: (input: string) => void,
+  onInputChange?: (text: string) => void,
+};
+
+
+const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
+  const { t } = useTranslation();
+  const {
+    isSearchServiceReachable, dropup,
+    onChange, onBlur, onFocus, onSubmit, onInputChange,
+  } = props;
+
+  const [searchError, setSearchError] = useState<Error | null>(null);
+  const [isShownHelp, setShownHelp] = useState(false);
+
+  const searchTyheaheadRef = useRef<IFocusable>(null);
+
+  // publish focus()
+  useImperativeHandle(ref, () => ({
+    focus() {
+      const instance = searchTyheaheadRef?.current;
+      if (instance != null) {
+        instance.focus();
+      }
+    },
+  }));
+
+  const placeholder = isSearchServiceReachable
+    ? 'Search ...'
+    : 'Error on Search Service';
+
+  const emptyLabel = (searchError != null)
+    ? 'Error on searching.'
+    : t('search.search page bodies');
+
+  return (
+    <SearchTypeahead
+      ref={searchTyheaheadRef}
+      dropup={dropup}
+      emptyLabel={emptyLabel}
+      placeholder={placeholder}
+      onChange={onChange}
+      onSubmit={onSubmit}
+      onInputChange={onInputChange}
+      onSearchError={err => setSearchError(err)}
+      onBlur={() => {
+        setShownHelp(false);
+        if (onBlur != null) {
+          onBlur();
+        }
+      }}
+      onFocus={() => {
+        setShownHelp(true);
+        if (onFocus != null) {
+          onFocus();
+        }
+      }}
+      helpElement={<SearchFormHelp isShownHelp={isShownHelp} isReachable={isSearchServiceReachable} />}
+      keywordOnInit={props.keyword}
+    />
+  );
+};
+
+export default forwardRef(SearchForm);

+ 4 - 1
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -31,11 +31,14 @@ class SearchPageForm extends React.Component {
   }
 
   render() {
+    const { appContainer } = this.props;
+    const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
+
     return (
       <div className="input-group mb-3 d-flex">
         <div className="flex-fill">
           <SearchForm
-            t={this.props.t}
+            isSearchServiceReachable={isSearchServiceReachable}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}

+ 0 - 274
packages/app/src/components/SearchTypeahead.jsx

@@ -1,274 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { noop } from 'lodash/noop';
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-
-import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
-
-import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-class SearchTypeahead extends React.Component {
-
-  constructor(props) {
-
-    super(props);
-
-    this.state = {
-      input: this.props.keywordOnInit,
-      pages: [],
-      isLoading: false,
-      searchError: null,
-    };
-
-    this.restoreInitialData = this.restoreInitialData.bind(this);
-    this.clearKeyword = this.clearKeyword.bind(this);
-    this.changeKeyword = this.changeKeyword.bind(this);
-    this.search = this.search.bind(this);
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onKeyDown = this.onKeyDown.bind(this);
-    this.dispatchSubmit = this.dispatchSubmit.bind(this);
-    this.getEmptyLabel = this.getEmptyLabel.bind(this);
-    this.getResetFormButton = this.getResetFormButton.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-    this.getTypeahead = this.getTypeahead.bind(this);
-  }
-
-  /**
-   * Get instance of AsyncTypeahead
-   */
-  getTypeahead() {
-    return this.typeahead ? this.typeahead.getInstance() : null;
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  /**
-   * Initialize keywordyword
-   */
-  restoreInitialData() {
-    this.changeKeyword(this.props.keywordOnInit);
-  }
-
-  /**
-   * clear keyword
-   */
-  clearKeyword(text) {
-    this.changeKeyword('');
-  }
-
-  /**
-   * change keyword
-   */
-  changeKeyword(text) {
-    // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
-    const instance = this.typeahead.getInstance();
-    instance.clear();
-    instance.setState({ text });
-  }
-
-  search(keyword) {
-
-    if (keyword === '') {
-      return;
-    }
-
-    this.setState({ isLoading: true });
-
-    this.props.appContainer.apiGet('/search', { q: keyword })
-      .then((res) => { this.onSearchSuccess(res) })
-      .catch((err) => { this.onSearchError(err) });
-  }
-
-  /**
-   * Callback function which is occured when search is exit successfully
-   * @param {*} pages
-   */
-  onSearchSuccess(res) {
-    this.setState({
-      isLoading: false,
-      pages: res.data,
-    });
-    if (this.props.onSearchSuccess != null) {
-      this.props.onSearchSuccess(res);
-    }
-  }
-
-  /**
-   * Callback function which is occured when search is exit abnormaly
-   * @param {*} err
-   */
-  onSearchError(err) {
-    this.setState({
-      isLoading: false,
-      searchError: err,
-    });
-    if (this.props.onSearchError != null) {
-      this.props.onSearchError(err);
-    }
-  }
-
-  onInputChange(text) {
-    this.setState({ input: text });
-    this.props.onInputChange(text);
-    if (text === '') {
-      this.setState({ pages: [] });
-    }
-  }
-
-  onKeyDown(event) {
-    if (event.keyCode === 13) {
-      this.dispatchSubmit();
-    }
-  }
-
-  dispatchSubmit() {
-    if (this.props.onSubmit != null) {
-      this.props.onSubmit(this.state.input);
-    }
-  }
-
-  getEmptyLabel() {
-    const { emptyLabel, helpElement } = this.props;
-    const { input } = this.state;
-
-    // show help element if empty
-    if (input.length === 0) {
-      return helpElement;
-    }
-
-    // use props.emptyLabel as is if defined
-    if (emptyLabel !== undefined) {
-      return this.props.emptyLabel;
-    }
-
-    let emptyLabelExceptError = 'No matches found on title...';
-    if (this.props.emptyLabelExceptError !== undefined) {
-      emptyLabelExceptError = this.props.emptyLabelExceptError;
-    }
-
-    return (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : emptyLabelExceptError;
-  }
-
-  /**
-   * Get restore form button to initialize button
-   */
-  getResetFormButton() {
-    const isClearBtn = this.props.behaviorOfResetBtn === 'clear';
-    const initialKeyword = isClearBtn ? '' : this.props.keywordOnInit;
-    const isHidden = this.state.input === initialKeyword;
-    const resetForm = isClearBtn ? this.clearKeyword : this.restoreInitialData;
-
-    return isHidden ? (
-      <span />
-    ) : (
-      <button type="button" className="btn btn-link search-clear" onMouseDown={resetForm}>
-        <i className="icon-close" />
-      </button>
-    );
-  }
-
-  renderMenuItemChildren(option, props, index) {
-    const page = option;
-    return (
-      <span>
-        <UserPicture user={page.lastUpdateUser} size="sm" noLink />
-        <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
-        <PageListMeta page={page} />
-      </span>
-    );
-  }
-
-  render() {
-    const defaultSelected = (this.props.keywordOnInit !== '')
-      ? [{ path: this.props.keywordOnInit }]
-      : [];
-    const inputProps = { autoComplete: 'off' };
-    if (this.props.inputName != null) {
-      inputProps.name = this.props.inputName;
-    }
-
-    const resetFormButton = this.getResetFormButton();
-
-    return (
-      <div className="search-typeahead">
-        <AsyncTypeahead
-          {...this.props}
-          id="search-typeahead-asynctypeahead"
-          ref={(c) => { this.typeahead = c }}
-          inputProps={inputProps}
-          isLoading={this.state.isLoading}
-          labelKey="path"
-          minLength={0}
-          options={this.state.pages} // Search result (Some page names)
-          promptText={this.props.helpElement}
-          emptyLabel={this.getEmptyLabel()}
-          align="left"
-          submitFormOnEnter
-          onSearch={this.search}
-          onInputChange={this.onInputChange}
-          onKeyDown={this.onKeyDown}
-          renderMenuItemChildren={this.renderMenuItemChildren}
-          caseSensitive={false}
-          defaultSelected={defaultSelected}
-          autoFocus={this.props.autoFocus}
-          onBlur={this.props.onBlur}
-          onFocus={this.props.onFocus}
-        />
-        {resetFormButton}
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SearchTypeaheadWrapper = withUnstatedContainers(SearchTypeahead, [AppContainer]);
-
-/**
- * Properties
- */
-SearchTypeahead.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  onSearchSuccess: PropTypes.func,
-  onSearchError:   PropTypes.func,
-  onChange:        PropTypes.func,
-  onBlur:          PropTypes.func,
-  onFocus:         PropTypes.func,
-  onSubmit:        PropTypes.func,
-  onInputChange:   PropTypes.func,
-  inputName:       PropTypes.string,
-  emptyLabel:      PropTypes.string,
-  emptyLabelExceptError: PropTypes.string,
-  placeholder:     PropTypes.string,
-  keywordOnInit:   PropTypes.string,
-  helpElement:     PropTypes.object,
-  autoFocus:       PropTypes.bool,
-  behaviorOfResetBtn: PropTypes.oneOf(['restore', 'clear']),
-};
-
-/**
- * Properties
- */
-SearchTypeahead.defaultProps = {
-  onSearchSuccess: noop,
-  onSearchError:   noop,
-  onChange:        noop,
-  placeholder:     '',
-  keywordOnInit:   '',
-  behaviorOfResetBtn: 'restore',
-  autoFocus:       false,
-  onInputChange: () => {},
-};
-
-export default SearchTypeaheadWrapper;

+ 242 - 0
packages/app/src/components/SearchTypeahead.tsx

@@ -0,0 +1,242 @@
+import React, {
+  FC, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
+  KeyboardEvent, useCallback, useRef, useState, MouseEvent,
+} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
+
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
+import { IFocusable } from '~/client/interfaces/focusable';
+import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
+import { apiGet } from '~/client/util/apiv1-client';
+import { IPage } from '~/interfaces/page';
+
+
+type ResetFormButtonProps = {
+  keywordOnInit: string,
+  input: string,
+  onReset: (e: MouseEvent<HTMLButtonElement>) => void,
+}
+
+const ResetFormButton: FC<ResetFormButtonProps> = (props: ResetFormButtonProps) => {
+  const isHidden = props.input.length === 0;
+
+  return isHidden ? (
+    <span />
+  ) : (
+    <button type="button" className="btn btn-link search-clear" onMouseDown={props.onReset}>
+      <i className="icon-close" />
+    </button>
+  );
+};
+
+
+type Props = TypeaheadProps & {
+  onSearchSuccess?: (res: IPage[]) => void,
+  onSearchError?: (err: Error) => void,
+  onSubmit?: (input: string) => void,
+  inputName?: string,
+  keywordOnInit?: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  helpElement?: any,
+};
+
+// see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
+type TypeaheadInstance = {
+  clear: () => void,
+  focus: () => void,
+  setState: ({ text: string }) => void,
+}
+type TypeaheadInstanceFactory = {
+  getInstance: () => TypeaheadInstance,
+}
+
+const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
+  const {
+    onSearchSuccess, onSearchError, onInputChange, onSubmit,
+    emptyLabel, helpElement,
+  } = props;
+
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  const [input, setInput] = useState(props.keywordOnInit!);
+  const [pages, setPages] = useState<IPage[]>();
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const [searchError, setSearchError] = useState<Error | null>(null);
+  const [isLoading, setLoading] = useState(false);
+
+  const typeaheadRef = useRef<TypeaheadInstanceFactory>(null);
+
+  const focusToTypeahead = () => {
+    const instance = typeaheadRef.current?.getInstance();
+    if (instance != null) {
+      instance.focus();
+    }
+  };
+
+  // publish focus()
+  useImperativeHandle(ref, () => ({
+    focus: focusToTypeahead,
+  }));
+
+  const changeKeyword = (text: string | undefined) => {
+    const instance = typeaheadRef.current?.getInstance();
+    if (instance != null) {
+      instance.clear();
+      instance.setState({ text });
+    }
+  };
+
+  const resetForm = (e: MouseEvent<HTMLButtonElement>) => {
+    e.preventDefault();
+
+    setInput('');
+    changeKeyword('');
+    setPages([]);
+
+    focusToTypeahead();
+
+    if (onInputChange != null) {
+      onInputChange('');
+    }
+  };
+
+  /**
+   * Callback function which is occured when search is exit successfully
+   */
+  const searchSuccessHandler = useCallback((res: AxiosResponse<IPage[]>) => {
+    setPages(res.data);
+
+    if (onSearchSuccess != null) {
+      onSearchSuccess(res.data);
+    }
+  }, [onSearchSuccess]);
+
+  /**
+   * Callback function which is occured when search is exit abnormaly
+   */
+  const searchErrorHandler = useCallback((err: Error) => {
+    setSearchError(err);
+
+    if (onSearchError != null) {
+      onSearchError(err);
+    }
+  }, [onSearchError]);
+
+  const search = useCallback(async(keyword: string) => {
+    if (keyword === '') {
+      return;
+    }
+
+    setLoading(true);
+
+    try {
+      const res = await apiGet('/search', { q: keyword }) as AxiosResponse<IPage[]>;
+      searchSuccessHandler(res);
+    }
+    catch (err) {
+      searchErrorHandler(err);
+    }
+    finally {
+      setLoading(false);
+    }
+
+  }, [searchErrorHandler, searchSuccessHandler]);
+
+  const inputChangeHandler = useCallback((text: string) => {
+    setInput(text);
+
+    if (onInputChange != null) {
+      onInputChange(text);
+    }
+
+    if (text === '') {
+      setPages([]);
+    }
+  }, [onInputChange]);
+
+  const keyDownHandler = useCallback((event: KeyboardEvent) => {
+    if (event.keyCode === 13) { // Enter key
+      if (onSubmit != null) {
+        onSubmit(input);
+      }
+    }
+  }, [input, onSubmit]);
+
+  const getEmptyLabel = () => {
+    // show help element if empty
+    if (input.length === 0) {
+      return helpElement;
+    }
+
+    // use props.emptyLabel as is if defined
+    if (emptyLabel !== undefined) {
+      return emptyLabel;
+    }
+
+    return false;
+  };
+
+  const defaultSelected = (props.keywordOnInit !== '')
+    ? [{ path: props.keywordOnInit }]
+    : [];
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const inputProps: any = { autoComplete: 'off' };
+  if (props.inputName != null) {
+    inputProps.name = props.inputName;
+  }
+
+  const renderMenuItemChildren = (page: IPage) => (
+    <span>
+      <UserPicture user={page.lastUpdateUser} size="sm" noLink />
+      <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
+      <PageListMeta page={page} />
+    </span>
+  );
+
+  return (
+    <div className="search-typeahead">
+      <AsyncTypeahead
+        {...props}
+        id="search-typeahead-asynctypeahead"
+        ref={typeaheadRef}
+        inputProps={inputProps}
+        isLoading={isLoading}
+        labelKey="path"
+        minLength={0}
+        options={pages} // Search result (Some page names)
+        promptText={props.helpElement}
+        emptyLabel={getEmptyLabel()}
+        align="left"
+        onSearch={search}
+        onInputChange={inputChangeHandler}
+        onKeyDown={keyDownHandler}
+        renderMenuItemChildren={renderMenuItemChildren}
+        caseSensitive={false}
+        defaultSelected={defaultSelected}
+        autoFocus={props.autoFocus}
+        onBlur={props.onBlur}
+        onFocus={props.onFocus}
+      />
+      <ResetFormButton
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        keywordOnInit={props.keywordOnInit!}
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        input={input}
+        onReset={resetForm}
+      />
+    </div>
+  );
+};
+
+const ForwardedSearchTypeahead = forwardRef(SearchTypeahead);
+
+ForwardedSearchTypeahead.defaultProps = {
+  placeholder: '',
+  keywordOnInit: '',
+  autoFocus: false,
+};
+
+export default ForwardedSearchTypeahead;

+ 1 - 1
packages/app/src/server/models/GlobalNotificationSetting/index.js

@@ -1,6 +1,6 @@
 const mongoose = require('mongoose');
 const nodePath = require('path');
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 
 /**
  * parent schema for GlobalNotificationSetting model

+ 1 - 1
packages/app/src/server/models/page.js

@@ -14,7 +14,7 @@ const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
 const differenceInYears = require('date-fns/differenceInYears');
 
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 const escapeStringRegexp = require('escape-string-regexp');
 
 const { isTopPage, isTrashPage } = pagePathUtils;

+ 1 - 1
packages/app/src/server/routes/apiv3/app-settings.js

@@ -7,7 +7,7 @@ const debug = require('debug')('growi:routes:admin');
 
 const express = require('express');
 
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 const { listLocaleIds } = require('~/utils/locale-utils');
 
 const router = express.Router();

+ 1 - 1
packages/app/src/server/routes/apiv3/pages.js

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const express = require('express');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 const mongoose = require('mongoose');
 
 const { body } = require('express-validator');

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

@@ -137,7 +137,7 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:page');
   const swig = require('swig-templates');
 
-  const pathUtils = require('growi-commons').pathUtils;
+  const { pathUtils } = require('@growi/core');
 
   const Page = crowi.model('Page');
   const User = crowi.model('User');

+ 1 - 1
packages/app/src/server/service/config-loader.ts

@@ -1,4 +1,4 @@
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
packages/app/src/server/service/slack-command-handler/create-page-service.js

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:service:CreatePageService');
 const { reshapeContentsBody, respond, markdownSectionBlock } = require('@growi/slack');
 const mongoose = require('mongoose');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 
 class CreatePageService {
 

+ 1 - 1
packages/app/src/server/util/middlewares.js

@@ -5,7 +5,7 @@ import loggerFactory from '~/utils/logger';
 // eslint-disable-next-line no-unused-vars
 
 const { formatDistanceStrict } = require('date-fns');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 const md5 = require('md5');
 const entities = require('entities');
 

+ 1 - 1
packages/app/src/server/util/swigFunctions.js

@@ -2,7 +2,7 @@ module.exports = function(crowi, req, locals) {
   const debug = require('debug')('growi:lib:swigFunctions');
   const stringWidth = require('string-width');
 
-  const { pathUtils } = require('growi-commons');
+  const { pathUtils } = require('@growi/core');
 
   const Page = crowi.model('Page');
   const User = crowi.model('User');

+ 5 - 1
packages/app/src/server/views/modal/shortcuts.html

@@ -26,6 +26,10 @@
                     <th>{{ t('modal_shortcuts.global.Edit Page') }}:</th>
                     <td><span class="key">E</span></td>
                   </tr>
+                  <tr>
+                    <th>{{ t('modal_shortcuts.global.Search') }}:</th>
+                    <td><span class="key">/</span></td>
+                  </tr>
                   <tr>
                     <th>{{ t('modal_shortcuts.global.Show Contributors') }}:</th>
                     <td>
@@ -79,7 +83,7 @@
               <table class="table">
                 <tr>
                   <th>{{ t('modal_shortcuts.commentform.Post') }}:</th>
-                  <td><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
+                  <td class="text-nowrap"><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
                 </tr>
                 <tr>
                   <th>{{ t('modal_shortcuts.editor.Delete Line') }}:</th>

+ 2 - 2
packages/app/src/server/views/widget/page_content.html

@@ -17,10 +17,10 @@
   data-page-is-not-creatable="false"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{% if page %}{{ page.slackChannels }}{% endif %}"
-  data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-created-at="{{ page.createdAt|datetz('Y/m/d H:i:s') }}"
   data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"
   data-page-last-update-username="{% if page && page.lastUpdateUser %}{{ page.lastUpdateUser.name }}{% endif %}"
-  data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-updated-at="{{ page.updatedAt|datetz('Y/m/d H:i:s') }}"
   data-page-delete-username="{% if page && page.deleteUser %}{{ page.deleteUser.name }}{% endif %}"
   data-page-deleted-at="{% if page && page.deletedAt %}{{ page.deletedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"

+ 1 - 1
packages/app/src/services/cdn-resources-service.js

@@ -4,7 +4,7 @@ import { resolveFromRoot } from '~/utils/project-dir-utils';
 const { URL } = require('url');
 const urljoin = require('url-join');
 
-const { envUtils } = require('growi-commons');
+const { envUtils } = require('@growi/core');
 
 const cdnLocalScriptRoot = 'public/static/js/cdn';
 const cdnLocalScriptWebRoot = '/static/js/cdn';

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

@@ -30,12 +30,12 @@ export const useRevisionCreatedAt = (initialData?: Nullable<any>): SWRResponse<N
   return useStaticSWR<Nullable<any>, Error>('revisionCreatedAt', initialData ?? null);
 };
 
-export const useCreatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('createdAt', initialData ?? null);
+export const useCurrentCreatedAt = (initialData?: Nullable<Date>): SWRResponse<Nullable<Date>, Error> => {
+  return useStaticSWR<Nullable<Date>, Error>('createdAt', initialData ?? null);
 };
 
-export const useUpdatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('updatedAt', initialData ?? null);
+export const useCurrentUpdatedAt = (initialData?: Nullable<Date>): SWRResponse<Nullable<Date>, Error> => {
+  return useStaticSWR<Nullable<Date>, Error>('updatedAt', initialData ?? null);
 };
 
 export const useDeletedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 9 - 0
packages/app/src/stores/ui.tsx

@@ -5,11 +5,13 @@ import useSWRImmutable from 'swr/immutable';
 
 import { Breakpoint, addBreakpointListener } from '@growi/ui';
 
+import { RefObject } from 'react';
 import { SidebarContentsType } from '~/interfaces/ui';
 import loggerFactory from '~/utils/logger';
 
 import { useStaticSWR } from './use-static-swr';
 import { useIsEditable } from './context';
+import { IFocusable } from '~/client/interfaces/focusable';
 
 const logger = loggerFactory('growi:stores:ui');
 
@@ -215,3 +217,10 @@ export const usePageCreateModalOpened = (isOpened?: boolean): SWRResponse<boolea
   const initialData = false;
   return useStaticSWR('isPageCreateModalOpened', isOpened || null, { fallbackData: initialData });
 };
+
+export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
+  return useStaticSWR(
+    'globalSearchTypeahead',
+    initialData ?? null,
+  );
+};

+ 11 - 10
packages/app/src/styles/_search.scss

@@ -55,8 +55,8 @@
 // input styles
 .grw-global-search {
   .search-clear {
-    top: 3px;
-    right: 26px;
+    top: 4px;
+    right: 4px;
   }
 
   .dropdown-toggle {
@@ -71,7 +71,7 @@
     border-top-right-radius: 40px;
     border-bottom-right-radius: 40px;
     .rbt-input-main {
-      padding-right: 58px;
+      padding-right: 36px;
       // corner radius
       border-top-right-radius: 40px;
       border-bottom-right-radius: 40px;
@@ -101,18 +101,19 @@
     }
   }
 
-  .btn-group-submit-search {
+  .grw-shortcut-key-indicator {
     position: absolute;
     top: 0;
-    right: 0;
-
-    z-index: 3;
+    right: 10px;
 
     display: flex;
     align-items: center;
-    justify-content: center;
-    width: 32px;
-    height: 32px;
+    height: 30px;
+
+    code {
+      padding-right: 0.4rem;
+      padding-left: 0.4rem;
+    }
   }
 }
 

+ 2 - 1
packages/app/src/styles/_sidebar.scss

@@ -22,7 +22,8 @@
   position: sticky;
   top: $grw-navbar-border-width;
 
-  height: 100vh;
+  // set the max value that should be taken when sticky
+  height: calc(100vh - $grw-navbar-border-width);
 
   .grw-navigation-resize-button {
     position: fixed;

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

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

+ 0 - 27
packages/core/README.md

@@ -1,27 +0,0 @@
-# growi-commons
-
-[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
-[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
-[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
-
-
-Overview
---------
-
-growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
-
-Install
---------
-
-1. install plugin
-
-    ```
-    $ npm install --save growi-commons
-    ```
-
-Documentation
-------------
-
-See https://docs.growi.org/api/commons/

+ 1 - 1
packages/core/package.json

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

+ 11 - 14
packages/core/src/index.js

@@ -1,22 +1,19 @@
 import * as _pathUtils from './utils/path-utils';
+import * as _envUtils from './utils/env-utils';
 import * as _pagePathUtils from './utils/page-path-utils';
 import * as _templateChecker from './utils/template-checker';
-
-// module.exports = {
-//   BasicInterceptor: require('./utils/basic-interceptor'),
-//   envUtils: require('./utils/env-utils'),
-//   // plugin
-//   customTagUtils: require('./plugin/util/custom-tag-utils'),
-//   TagCacheManager: require('./plugin/service/tag-cache-manager'),
-//   // service
-//   LocalStorageManager: require('./service/localstorage-manager'),
-// };
-
-export * from './plugin/interfaces/plugin-definition-v4';
-export * from './models/devided-page-path';
-export * from './utils/mongoose-utils';
+import * as _customTagUtils from './plugin/util/custom-tag-utils';
 
 // export utils
 export const pathUtils = _pathUtils;
+export const envUtils = _envUtils;
 export const pagePathUtils = _pagePathUtils;
 export const templateChecker = _templateChecker;
+export const customTagUtils = _customTagUtils;
+
+export * from './plugin/interfaces/plugin-definition-v4';
+export * from './plugin/service/tag-cache-manager';
+export * from './models/devided-page-path';
+export * from './service/localstorage-manager';
+export * from './utils/basic-interceptor';
+export * from './utils/mongoose-utils';

+ 2 - 4
packages/core/src/plugin/service/tag-cache-manager.js

@@ -1,9 +1,9 @@
-const LocalStorageManager = require('../../service/localstorage-manager');
+import { LocalStorageManager } from '../../service/localstorage-manager';
 
 /**
  * Service Class for caching React state and TagContext
  */
-class TagCacheManager {
+export class TagCacheManager {
 
   /**
    * @callback generateCacheKey
@@ -67,5 +67,3 @@ class TagCacheManager {
   }
 
 }
-
-module.exports = TagCacheManager;

+ 1 - 3
packages/core/src/service/localstorage-manager.js

@@ -1,5 +1,5 @@
 let _instance = null;
-class LocalStorageManager {
+export class LocalStorageManager {
 
   static getInstance() {
     if (_instance == null) {
@@ -52,5 +52,3 @@ class LocalStorageManager {
   }
 
 }
-
-module.exports = LocalStorageManager;

+ 1 - 3
packages/core/src/utils/basic-interceptor.js

@@ -1,7 +1,7 @@
 /**
  * Basic Interceptor class
  */
-class BasicInterceptor {
+export class BasicInterceptor {
 
   /**
    * getter for id
@@ -43,5 +43,3 @@ class BasicInterceptor {
   }
 
 }
-
-module.exports = BasicInterceptor;

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

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

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import RefsContext from '../RefsContext';
 import GalleryContext from '../GalleryContext';

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js

@@ -1,4 +1,4 @@
-import { customTagUtils, BasicInterceptor } from 'growi-commons';
+import { customTagUtils, BasicInterceptor } from '@growi/core';
 
 import TagCacheManagerFactory from '../TagCacheManagerFactory';
 

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/RefsContext.js

@@ -1,6 +1,6 @@
 import * as url from 'url';
 
-import { customTagUtils, pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from '@growi/core';
 
 const { TagContext, ArgsParser, OptionParser } = customTagUtils;
 

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js

@@ -1,4 +1,4 @@
-import { TagCacheManager } from 'growi-commons';
+import { TagCacheManager } from '@growi/core';
 
 const STATE_CACHE_NS = 'refs-state-cache';
 

+ 1 - 1
packages/plugin-attachment-refs/src/server/routes/refs.js

@@ -1,6 +1,6 @@
 import loggerFactory from '../../utils/logger';
 
-const { customTagUtils } = require('growi-commons');
+const { customTagUtils } = require('@growi/core');
 
 const { OptionParser } = customTagUtils;
 

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

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

+ 1 - 1
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import * as url from 'url';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 // eslint-disable-next-line no-unused-vars
 import styles from '../../css/index.css';

+ 1 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 import { PageListMeta } from '@growi/ui';
 

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import { TagCacheManagerFactory } from '../TagCacheManagerFactory';
 

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import { LsxContext } from '../LsxContext';
 import { Lsx } from '../../components/Lsx';

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js

@@ -1,5 +1,5 @@
 import ReactDOM from 'react-dom';
-import { customTagUtils, BasicInterceptor } from 'growi-commons';
+import { customTagUtils, BasicInterceptor } from '@growi/core';
 
 /**
  * The interceptor for lsx

+ 1 - 1
packages/plugin-lsx/src/client/js/util/LsxContext.js

@@ -1,6 +1,6 @@
 import * as url from 'url';
 
-import { customTagUtils, pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from '@growi/core';
 
 const { TagContext, ArgsParser, OptionParser } = customTagUtils;
 

+ 1 - 18
packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js

@@ -1,29 +1,12 @@
-import { TagCacheManager } from 'growi-commons';
+import { TagCacheManager } from '@growi/core';
 
 const LSX_STATE_CACHE_NS = 'lsx-state-cache';
 
 
-// validate growi-commons version
-function validateGrowiCommonsVersion() {
-  // TagCacheManager was created on growi-commons@4.0.7
-  if (TagCacheManager == null) {
-    throw new Error(
-      'This version of \'growi-plugin-lsx\' requires \'growi-commons >= 4.0.7\'.\n'
-      + 'To resolve this, please process  either a) or b).\n'
-      + '\n'
-      + 'a) Use \'growi-plugin-lsx@3.0.0\'\n'
-      + 'b) Edit \'package.json\' of growi and upgrade \'growi-commons\' to v4.0.7 or above.',
-    );
-  }
-}
-
-
 let _instance;
 export class TagCacheManagerFactory {
 
   static getInstance() {
-    validateGrowiCommonsVersion();
-
     if (_instance == null) {
       // create generateCacheKey implementation
       const generateCacheKey = (lsxContext) => {

+ 1 - 1
packages/plugin-lsx/src/server/routes/lsx.js

@@ -1,4 +1,4 @@
-const { customTagUtils } = require('growi-commons');
+const { customTagUtils } = require('@growi/core');
 
 const { OptionParser } = customTagUtils;
 

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

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

+ 1 - 1
packages/slack/package.json

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

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

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

+ 0 - 27
packages/ui/README.md

@@ -1,27 +0,0 @@
-# growi-commons
-
-[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
-[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
-[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
-
-
-Overview
---------
-
-growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
-
-Install
---------
-
-1. install plugin
-
-    ```
-    $ npm install --save growi-commons
-    ```
-
-Documentation
-------------
-
-See https://docs.growi.org/api/commons/

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "4.5.3",
+  "version": "4.5.4-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

Разница между файлами не показана из-за своего большого размера
+ 349 - 272
yarn.lock


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