فهرست منبع

Merge branch 'dev/5.0.x' into rc/5.0.x

yohei0125 4 سال پیش
والد
کامیت
4548ad32d5
26فایلهای تغییر یافته به همراه241 افزوده شده و 180 حذف شده
  1. 4 1
      packages/app/resource/locales/en_US/translation.json
  2. 4 1
      packages/app/resource/locales/ja_JP/translation.json
  3. 4 1
      packages/app/resource/locales/zh_CN/translation.json
  4. 1 2
      packages/app/src/client/services/AdminHomeContainer.js
  5. 7 2
      packages/app/src/client/services/ContextExtractor.tsx
  6. 81 86
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  7. 5 1
      packages/app/src/components/Common/ClosableTextInput.tsx
  8. 27 23
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  9. 3 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  10. 21 14
      packages/app/src/components/Navbar/GrowiNavbarBottom.jsx
  11. 1 1
      packages/app/src/components/Page/CopyDropdown.jsx
  12. 9 6
      packages/app/src/components/Page/RevisionRenderer.jsx
  13. 1 1
      packages/app/src/components/RevisionComparer/RevisionComparer.jsx
  14. 6 0
      packages/app/src/components/SearchPage.jsx
  15. 1 1
      packages/app/src/components/SearchPage/SearchControl.tsx
  16. 1 1
      packages/app/src/components/SearchPage/SearchPageLayout.tsx
  17. 1 1
      packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx
  18. 6 5
      packages/app/src/components/SearchPage/SearchResultListItem.tsx
  19. 1 1
      packages/app/src/components/SearchPage/SortControl.tsx
  20. 5 4
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  21. 3 0
      packages/app/src/stores/context.tsx
  22. 1 6
      packages/app/src/styles/_page-tree.scss
  23. 3 8
      packages/app/src/styles/_search.scss
  24. 25 0
      packages/app/src/styles/theme/_apply-colors-dark.scss
  25. 4 1
      packages/app/src/styles/theme/_apply-colors-light.scss
  26. 16 12
      packages/app/src/styles/theme/_apply-colors.scss

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

@@ -11,6 +11,7 @@
   "phone":"Smartphone",
   "tablet":"Tablet",
   "Click to copy": "Click to copy",
+  "Rename" : "Rename",
   "Move/Rename": "Move/Rename",
   "Moved": "Moved",
   "Redirected": "Redirected",
@@ -39,6 +40,7 @@
   "account_id": "Account Id",
   "Update": "Update",
   "Update Page": "Update Page",
+  "Error": "Error",
   "Warning": "Warning",
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
@@ -168,7 +170,8 @@
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
-    "invalid_syntax": "The syntax of %s is invalid."
+    "invalid_syntax": "The syntax of %s is invalid.",
+    "title_required": "Title is required."
   },
   "not_found_page": {
     "Create Page": "Create Page",

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

@@ -11,6 +11,7 @@
   "phone":"スマホ",
   "tablet":"タブレット",
   "Click to copy": "クリックでコピー",
+  "Rename": "名前変更",
   "Move/Rename": "移動/名前変更",
   "Moved": "移動しました",
   "Redirected": "リダイレクトされました",
@@ -40,6 +41,7 @@
   "Initialize": "初期化",
   "Update": "更新",
   "Update Page": "ページを更新",
+  "Error": "エラー",
   "Warning": "注意",
   "Sign in": "ログイン",
   "Sign up is here": "新規登録はこちら",
@@ -170,7 +172,8 @@
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
-    "invalid_syntax": "%sの構文が不正です"
+    "invalid_syntax": "%sの構文が不正です",
+    "title_required": "タイトルを入力してください"
   },
   "not_found_page": {
     "Create Page": "ページを作成する",

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

@@ -12,6 +12,7 @@
   "tablet":"平板",
 	"Login": "登录",
 	"Click to copy": "点击复制",
+  "Rename": "重命名",
 	"Move/Rename": "移动/重命名",
 	"Moved": "移动",
 	"Redirected": "重定向",
@@ -41,6 +42,7 @@
 	"Initialize": "初始化",
   "Update": "更新",
 	"Update Page": "更新本页",
+	"Error": "误差",
 	"Warning": "警告",
   "Sign in": "登录",
 	"Sign up is here": "注册",
@@ -168,7 +170,8 @@
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
-		"invalid_syntax": "%s的语法无效。"
+		"invalid_syntax": "%s的语法无效。",
+    "title_required": "标题是必需的。"
   },
   "not_found_page": {
     "Create Page": "创建页面",

+ 1 - 2
packages/app/src/client/services/AdminHomeContainer.js

@@ -25,7 +25,6 @@ export default class AdminHomeContainer extends Container {
     this.timer = null;
 
     this.state = {
-      retrieveError: null,
       growiVersion: '',
       nodeVersion: '',
       npmVersion: '',
@@ -69,7 +68,7 @@ export default class AdminHomeContainer extends Container {
     }
     catch (err) {
       logger.error(err);
-      toastError(new Error('Failed to fetch data'));
+      throw new Error('Failed to retrive AdminHome data');
     }
   }
 

+ 7 - 2
packages/app/src/client/services/ContextExtractor.tsx

@@ -6,10 +6,10 @@ import {
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
-  useSlackChannels, useNotFoundTargetPathOrId,
+  useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage,
 } from '../../stores/context';
 import {
-  useIsDeviceSmallerThanMd,
+  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
@@ -65,6 +65,7 @@ const ContextExtractorOnce: FC = () => {
   const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
   const notFoundTargetPathOrId = JSON.parse(notFoundContent?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
   const slackChannels = mainContent?.getAttribute('data-slack-channels') || '';
+  const isSearchPage = document.getElementById('search-page') != null;
 
   /*
    * use static swr
@@ -108,6 +109,7 @@ const ContextExtractorOnce: FC = () => {
   useRevisionAuthor(revisionAuthor);
   useTargetAndAncestors(targetAndAncestors);
   useNotFoundTargetPathOrId(notFoundTargetPathOrId);
+  useIsSearchPage(isSearchPage);
 
   // Navigation
   usePreferDrawerModeByUser();
@@ -122,6 +124,9 @@ const ContextExtractorOnce: FC = () => {
   // Editor
   useSlackChannels(slackChannels);
 
+  // SearchResult
+  useIsDeviceSmallerThanLg();
+
   return null;
 };
 

+ 81 - 86
packages/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -1,6 +1,6 @@
-import React, { Fragment } from 'react';
+import React, { useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
 import loggerFactory from '~/utils/logger';
@@ -10,117 +10,112 @@ import { toastError } from '~/client/util/apiNotification';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import SystemInfomationTable from './SystemInfomationTable';
 import InstalledPluginTable from './InstalledPluginTable';
 import EnvVarsTable from './EnvVarsTable';
 
 const logger = loggerFactory('growi:admin');
 
-class AdminHome extends React.Component {
-
-  async componentDidMount() {
-    const { adminHomeContainer } = this.props;
+const AdminHome = (props) => {
+  const { adminHomeContainer } = props;
+  const { t } = useTranslation();
+  const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
+  const fetchAdminHomeData = useCallback(async() => {
     try {
       await adminHomeContainer.retrieveAdminHomeData();
     }
     catch (err) {
       toastError(err);
-      adminHomeContainer.setState({ retrieveError: err });
       logger.error(err);
     }
-  }
-
-  render() {
-    const { t, adminHomeContainer } = this.props;
-    const { isV5Compatible } = adminHomeContainer.state;
-
-    let alertStyle = 'alert-info';
-    if (isV5Compatible == null) alertStyle = 'alert-warning';
-
-    return (
-      <Fragment>
-        {
-          // not show if true
-          !isV5Compatible
-          && (
-            <div className={`alert ${alertStyle}`}>
-              {t('admin:v5_page_migration.migration_desc')}
-              <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-                <i className="fa fa-link ml-1" aria-hidden="true"></i>
-                <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
-              </a>
-            </div>
-          )
-        }
-        <p>
-          {t('admin:admin_top.wiki_administrator')}
-          <br></br>
-          {t('admin:admin_top.assign_administrator')}
-        </p>
-
-        <div className="row mb-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
-            <SystemInfomationTable />
+  }, [adminHomeContainer]);
+
+  useEffect(() => {
+    fetchAdminHomeData();
+  }, [fetchAdminHomeData]);
+
+  return (
+    <>
+      {
+      // Alert message will be displayed in case that V5 migration has not been compleated
+        (migrationStatus != null && !migrationStatus.isV5Compatible)
+        && (
+          <div className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}>
+            {t('admin:v5_page_migration.migration_desc')}
+            <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
+              <i className="fa fa-link ml-1" aria-hidden="true"></i>
+              <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
+            </a>
           </div>
+        )
+      }
+      <p>
+        {t('admin:admin_top.wiki_administrator')}
+        <br></br>
+        {t('admin:admin_top.assign_administrator')}
+      </p>
+
+      <div className="row mb-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
+          <SystemInfomationTable />
         </div>
+      </div>
 
-        <div className="row mb-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
-            <InstalledPluginTable />
-          </div>
+      <div className="row mb-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
+          <InstalledPluginTable />
         </div>
-
-        <div className="row mb-5">
-          <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('admin:admin_top.list_of_env_vars')}</h2>
-            <p>{t('admin:admin_top.env_var_priority')}</p>
-            {/* eslint-disable-next-line react/no-danger */}
-            <p dangerouslySetInnerHTML={{ __html: t('admin:admin_top.about_security') }} />
-            {adminHomeContainer.state.envVars && <EnvVarsTable envVars={adminHomeContainer.state.envVars} />}
-          </div>
+      </div>
+
+      <div className="row mb-5">
+        <div className="col-md-12">
+          <h2 className="admin-setting-header">{t('admin:admin_top.list_of_env_vars')}</h2>
+          <p>{t('admin:admin_top.env_var_priority')}</p>
+          {/* eslint-disable-next-line react/no-danger */}
+          <p dangerouslySetInnerHTML={{ __html: t('admin:admin_top.about_security') }} />
+          {adminHomeContainer.state.envVars && <EnvVarsTable envVars={adminHomeContainer.state.envVars} />}
         </div>
-
-        <div className="row mb-5">
-          <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('admin:admin_top.bug_report')}</h2>
-            <div className="d-flex align-items-center">
-              <CopyToClipboard
-                text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
-                onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
-              >
-                <button id="prefilledHostInformationButton" type="button" className="btn btn-primary">
-                  {t('admin:admin_top:copy_prefilled_host_information:default')}
-                </button>
-              </CopyToClipboard>
-              <Tooltip
-                placement="bottom"
-                isOpen={adminHomeContainer.state.copyState === adminHomeContainer.copyStateValues.DONE}
-                target="prefilledHostInformationButton"
-                fade={false}
-              >
-                {t('admin:admin_top:copy_prefilled_host_information:done')}
-              </Tooltip>
-              {/* eslint-disable-next-line react/no-danger */}
-              <span className="ml-2" dangerouslySetInnerHTML={{ __html: t('admin:admin_top:submit_bug_report') }} />
-            </div>
+      </div>
+
+      <div className="row mb-5">
+        <div className="col-md-12">
+          <h2 className="admin-setting-header">{t('admin:admin_top.bug_report')}</h2>
+          <div className="d-flex align-items-center">
+            <CopyToClipboard
+              text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
+              onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
+            >
+              <button id="prefilledHostInformationButton" type="button" className="btn btn-primary">
+                {t('admin:admin_top:copy_prefilled_host_information:default')}
+              </button>
+            </CopyToClipboard>
+            <Tooltip
+              placement="bottom"
+              isOpen={adminHomeContainer.state.copyState === adminHomeContainer.copyStateValues.DONE}
+              target="prefilledHostInformationButton"
+              fade={false}
+            >
+              {t('admin:admin_top:copy_prefilled_host_information:done')}
+            </Tooltip>
+            {/* eslint-disable-next-line react/no-danger */}
+            <span className="ml-2" dangerouslySetInnerHTML={{ __html: t('admin:admin_top:submit_bug_report') }} />
           </div>
         </div>
-      </Fragment>
-    );
-  }
+      </div>
+    </>
+  );
+};
 
-}
 
 const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AppContainer, AdminHomeContainer]);
 
 AdminHome.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
 };
 
-export default withTranslation()(AdminHomeWrapper);
+export default AdminHomeWrapper;

+ 5 - 1
packages/app/src/components/Common/ClosableTextInput.tsx

@@ -1,6 +1,7 @@
 import React, {
   FC, memo, useEffect, useRef, useState,
 } from 'react';
+import { useTranslation } from 'react-i18next';
 
 export const AlertType = {
   WARNING: 'warning',
@@ -23,6 +24,7 @@ type ClosableTextInputProps = {
 }
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
+  const { t } = useTranslation();
   const inputRef = useRef<HTMLInputElement>(null);
 
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
@@ -81,8 +83,10 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
 
     const alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
     const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
+    const alertTextStyle = alertType === AlertType.ERROR ? 'text-danger' : 'text-warning';
+    const translation = alertType === AlertType.ERROR ? 'Error' : 'Warning';
     return (
-      <p className="text-danger text-center mt-1">{alertType}: {alertMessage}</p>
+      <p className={`${alertTextStyle} text-center mt-1`}>{t(translation)}: {alertMessage}</p>
     );
   };
 

+ 27 - 23
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -1,4 +1,7 @@
 import React, { FC } from 'react';
+import {
+  UncontrolledDropdown, DropdownMenu, DropdownToggle, DropdownItem,
+} from 'reactstrap';
 
 import toastr from 'toastr';
 import { useTranslation } from 'react-i18next';
@@ -25,15 +28,11 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
     }
   };
   return (
-    <>
-      <button
-        type="button"
-        className="btn-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded grw-btn-page-management py-0 px-2"
-        data-toggle="dropdown"
-      >
-        <i className="fa fa-ellipsis-v text-muted p-1"></i>
-      </button>
-      <div className="dropdown-menu dropdown-menu-right">
+    <UncontrolledDropdown>
+      <DropdownToggle color="transparent" className="btn-link border-0 rounded grw-btn-page-management p-0">
+        <i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
+      </DropdownToggle>
+      <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
 
         {/* TODO: if there is the following button in XD add it here
         <button
@@ -54,40 +53,45 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
         */}
 
         {/* TODO: show dropdown when permalink section is implemented */}
+
         {!isEnableActions && (
-          <p className="dropdown-item">
-            {t('search_result.currently_not_implemented')}
-          </p>
+          <DropdownItem>
+            <p>
+              {t('search_result.currently_not_implemented')}
+            </p>
+          </DropdownItem>
         )}
         {isEnableActions && (
-          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+          <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
             <i className="icon-fw icon-star"></i>
             {t('Add to bookmark')}
-          </button>
+          </DropdownItem>
         )}
         {isEnableActions && (
-          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+          <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
             <i className="icon-fw icon-docs"></i>
             {t('Duplicate')}
-          </button>
+          </DropdownItem>
         )}
         {isEnableActions && (
-          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+          <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
             <i className="icon-fw  icon-action-redo"></i>
             {t('Move/Rename')}
-          </button>
+          </DropdownItem>
         )}
         {isDeletable && isEnableActions && (
           <>
-            <div className="dropdown-divider"></div>
-            <button className="dropdown-item text-danger pt-2" type="button" onClick={deleteButtonHandler}>
+            <DropdownItem divider />
+            <DropdownItem className="text-danger pt-2" onClick={deleteButtonHandler}>
               <i className="icon-fw icon-trash"></i>
               {t('Delete')}
-            </button>
+            </DropdownItem>
           </>
         )}
-      </div>
-    </>
+      </DropdownMenu>
+
+
+    </UncontrolledDropdown>
   );
 
 };

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

@@ -8,6 +8,7 @@ import { UncontrolledTooltip } from 'reactstrap';
 import AppContainer from '~/client/services/AppContainer';
 import { IUser } from '~/interfaces/user';
 import { useIsDeviceSmallerThanMd, useCreateModalStatus } from '~/stores/ui';
+import { useIsSearchPage } from '~/stores/context';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import GrowiLogo from '../Icons/GrowiLogo';
@@ -84,6 +85,7 @@ const GrowiNavbar = (props) => {
   const { crowi, isSearchServiceConfigured } = appContainer.config;
 
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isSearchPage } = useIsSearchPage();
 
   return (
     <>
@@ -105,7 +107,7 @@ const GrowiNavbar = (props) => {
         <Confidential confidential={crowi.confidential}></Confidential>
       </ul>
 
-      { isSearchServiceConfigured && !isDeviceSmallerThanMd && (
+      { isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
         <div className="grw-global-search grw-global-search-top position-absolute">
           <GlobalSearch />
         </div>

+ 21 - 14
packages/app/src/components/Navbar/GrowiNavbarBottom.jsx

@@ -1,7 +1,9 @@
 import React from 'react';
+import PropTypes from 'prop-types';
+
 
 import { useCreateModalStatus, useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath, useIsSearchPage } from '~/stores/context';
 
 import GlobalSearch from './GlobalSearch';
 
@@ -11,6 +13,7 @@ const GrowiNavbarBottom = (props) => {
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { open: openCreateModal } = useCreateModalStatus();
   const { data: currentPagePath } = useCurrentPagePath();
+  const { data: isSearchPage } = useIsSearchPage();
 
   const additionalClasses = ['grw-navbar-bottom'];
   if (isDrawerOpened) {
@@ -20,7 +23,7 @@ const GrowiNavbarBottom = (props) => {
   return (
     <div className="d-md-none d-edit-none fixed-bottom">
 
-      { isDeviceSmallerThanMd && (
+      { isDeviceSmallerThanMd && !isSearchPage && (
         <div id="grw-global-search-collapse" className="grw-global-search collapse bg-dark">
           <div className="p-3">
             <GlobalSearch dropup />
@@ -31,7 +34,7 @@ const GrowiNavbarBottom = (props) => {
       <div className={`navbar navbar-expand navbar-dark bg-primary px-0 ${additionalClasses.join(' ')}`}>
 
         <ul className="navbar-nav w-100">
-          <li className="nav-item">
+          <li className="nav-item mr-auto">
             <a
               role="button"
               className="nav-link btn-lg"
@@ -40,17 +43,21 @@ const GrowiNavbarBottom = (props) => {
               <i className="icon-menu"></i>
             </a>
           </li>
-          <li className="nav-item mx-auto">
-            <a
-              role="button"
-              className="nav-link btn-lg"
-              data-target="#grw-global-search-collapse"
-              data-toggle="collapse"
-            >
-              <i className="icon-magnifier"></i>
-            </a>
-          </li>
-          <li className="nav-item">
+          {
+            !isSearchPage && (
+              <li className="nav-item">
+                <a
+                  role="button"
+                  className="nav-link btn-lg"
+                  data-target="#grw-global-search-collapse"
+                  data-toggle="collapse"
+                >
+                  <i className="icon-magnifier"></i>
+                </a>
+              </li>
+            )
+          }
+          <li className="nav-item ml-auto">
             <a
               role="button"
               className="nav-link btn-lg"

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

@@ -118,7 +118,7 @@ const CopyDropdown = (props) => {
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>
 
-        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: null } }}>
+        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
 
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">

+ 9 - 6
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -60,20 +60,23 @@ class LegacyRevisionRenderer extends React.PureComponent {
    * @param {string} keywords
    */
   getHighlightedBody(body, keywords) {
-    let returnBody = body;
+    const returnBody = body;
 
-    keywords.replace(/"/g, '').split(' ').forEach((keyword) => {
+    const normalizedKeywordsArray = [];
+    keywords.replace(/"/g, '').split(/[\u{20}\u{3000}]/u).forEach((keyword, i) => { // split by both full-with and half-width space
       if (keyword === '') {
         return;
       }
       const k = keyword
-        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
         .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-      const keywordExp = new RegExp(`(${k}(?!(.*?")))`, 'ig');
-      returnBody = returnBody.replace(keywordExp, '<em class="highlighted-keyword">$&</em>');
+      normalizedKeywordsArray.push(k);
     });
 
-    return returnBody;
+    const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
+    const keywordExp = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // exclude html tag as well https://regex101.com/r/dznxyh/1
+
+    return returnBody.replace(keywordExp, '<em class="highlighted-keyword">$&</em>');
   }
 
   async renderHtml() {

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

@@ -74,7 +74,7 @@ const RevisionComparer = (props) => {
           >
             <i className="ti-clipboard"></i>
           </DropdownToggle>
-          <DropdownMenu positionFixed right modifiers={{ preventOverflow: { boundariesElement: null } }}>
+          <DropdownMenu positionFixed right modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
             {/* Page path URL */}
             <CopyToClipboard text={pagePathUrl()}>
               <DropdownItem className="px-3">

+ 6 - 0
packages/app/src/components/SearchPage.jsx

@@ -150,6 +150,12 @@ class SearchPage extends React.Component {
   // todo: refactoring
   // refs: https://redmine.weseek.co.jp/issues/82139
   async search(data) {
+    // reset following states when search runs
+    this.setState({
+      selectedPagesIdList: new Set(),
+      selectAllCheckboxType: CheckboxType.NONE_CHECKED,
+    });
+
     const keyword = data.keyword;
     if (keyword === '') {
       this.setState({

+ 1 - 1
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -91,7 +91,7 @@ const SearchControl: FC <Props> = (props: Props) => {
 
   return (
     <div className="position-sticky fixed-top shadow-sm">
-      <div className="search-page-nav d-flex py-3 align-items-center">
+      <div className="grw-search-page-nav d-flex py-3 align-items-center">
         <div className="flex-grow-1 mx-4">
           <SearchPageFormTypeAny
             keyword={props.searchingKeyword}

+ 1 - 1
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -47,7 +47,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
               </div>
               <div className="input-group search-result-select-group ml-4 d-lg-flex d-none">
                 <div className="input-group-prepend">
-                  <label className="input-group-text text-secondary" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
+                  <label className="input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
                 </div>
                 <select
                   defaultValue={props.pagingLimit}

+ 1 - 1
packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx

@@ -32,7 +32,7 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
   const { isSharedUser } = appContainer;
   const isAbleToShowPageManagement = !(isTrashPage(path)) && !isSharedUser;
   return (
-    <div className="position-sticky fixed-top shadow-sm search-result-content-nav">
+    <div className="shadow-sm search-result-content-nav">
       <div className={`grw-subnav container-fluid d-flex align-items-start justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
         {/* Left side */}
         <div className="grw-path-nav-container">

+ 6 - 5
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -49,11 +49,10 @@ const SearchResultListItem: FC<Props> = memo((props:Props) => {
   );
 
   const responsiveListStyleClass = `${isDeviceSmallerThanLg ? '' : `list-group-item-action ${isSelected ? 'active' : ''}`}`;
-
   return (
     <li
       key={pageData._id}
-      className={`w-100 page-list-li search-result-item border-bottom ${responsiveListStyleClass}`}
+      className={`w-100 grw-search-result-item border-bottom ${responsiveListStyleClass}`}
     >
       <div
         className="h-100 text-break"
@@ -77,8 +76,10 @@ const SearchResultListItem: FC<Props> = memo((props:Props) => {
           <div className="search-item-text p-md-3 pl-2 py-3 pr-3 flex-grow-1">
             {/* page path */}
             <h6 className="mb-1 py-1">
-              <i className="icon-fw icon-home"></i>
-              <a href={pagePath.isRoot ? pagePath.latter : pagePath.former}>{pagePathElem}</a>
+              <a href={pagePath.isRoot ? pagePath.latter : pagePath.former}>
+                <i className="icon-fw icon-home"></i>
+                {pagePathElem}
+              </a>
             </h6>
             <div className="d-flex align-items-center mb-2">
               {/* Picture */}
@@ -106,7 +107,7 @@ const SearchResultListItem: FC<Props> = memo((props:Props) => {
                 />
               </div>
             </div>
-            <div className="search-result-list-snippet py-1">
+            <div className="grw-search-result-list-snippet py-1">
               <Clamp lines={2}>
                 {
                   pageMeta.elasticSearchResult != null && pageMeta.elasticSearchResult?.snippet.length !== 0 ? (

+ 1 - 1
packages/app/src/components/SearchPage/SortControl.tsx

@@ -29,7 +29,7 @@ const SortControl: FC <Props> = (props: Props) => {
     <>
       <div className="input-group">
         <div className="input-group-prepend">
-          <div className="input-group-text border" id="btnGroupAddon">
+          <div className="input-group-text border text-muted" id="btnGroupAddon">
             {renderOrderIcon(props.order)}
           </div>
         </div>

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

@@ -86,8 +86,9 @@ const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
 const ItemCount: FC = () => {
   return (
     <>
-      <span className="grw-pagetree-count badge badge-pill badge-light">
+      <span className="grw-pagetree-count badge badge-pill badge-light text-muted">
         {/* TODO: consider to show the number of children pages */}
+        00
       </span>
     </>
   );
@@ -139,8 +140,8 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const inputValidator = (title: string | null): AlertInfo | null => {
     if (title == null || title === '') {
       return {
-        type: AlertType.ERROR,
-        message: t('Page title is required'),
+        type: AlertType.WARNING,
+        message: t('form_validation.title_required'),
       };
     }
 
@@ -191,7 +192,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           </div>
         </button>
         <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
-          <p className={`grw-pagetree-title m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path as string) || '/'}</p>
+          <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path as string) || '/'}</p>
         </a>
         <div className="grw-pagetree-count-wrapper">
           <ItemCount />

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

@@ -124,6 +124,9 @@ export const useSlackChannels = (initialData?: Nullable<any>): SWRResponse<Nulla
   return useStaticSWR<Nullable<any>, Error>('slackChannels', initialData ?? null);
 };
 
+export const useIsSearchPage = (initialData?: Nullable<any>) : SWRResponse<Nullable<any>, Error> => {
+  return useStaticSWR<Nullable<any>, Error>('isSearchPage', initialData ?? null);
+};
 /** **********************************************************
  *                     Computed contexts
  *********************************************************** */

+ 1 - 6
packages/app/src/styles/_page-tree.scss

@@ -34,11 +34,6 @@ $grw-pagetree-item-padding-left: 10px;
       width: 100%;
       overflow: hidden;
       text-decoration: none;
-
-      .grw-pagetree-title {
-        overflow: hidden;
-        text-overflow: ellipsis;
-      }
     }
 
     .grw-pagetree-count-wrapper {
@@ -49,7 +44,7 @@ $grw-pagetree-item-padding-left: 10px;
       }
 
       .grw-pagetree-count {
-        padding: 0.3rem 1rem;
+        padding: 0.1rem 0.3rem;
       }
     }
   }

+ 3 - 8
packages/app/src/styles/_search.scss

@@ -1,7 +1,3 @@
-.search-page-nav {
-  background-color: #f7f7f7;
-}
-
 .search-group-submit-button {
   position: absolute;
   top: 0;
@@ -182,6 +178,7 @@
   .search-result-list {
     position: sticky;
     top: 0px;
+    z-index: 10; // to avoid dropdown menu in this class to be placed behind elements displayed on the right pane
 
     .search-result-list-scroll {
       // subtract the height of GrowiNavbar + (SearchControl component + other factors)
@@ -189,7 +186,8 @@
       overflow-y: scroll;
     }
     .nav.nav-pills {
-      > .page-list-li {
+      > .grw-search-result-item {
+        min-height: 136px;
         &.active {
           border-left: solid 3px transparent;
           .search-item-checkbox {
@@ -221,9 +219,6 @@
         }
       }
     }
-    .search-result-item {
-      min-height: 136px;
-    }
 
     .search-result-meta {
       font-weight: bold;

+ 25 - 0
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -60,6 +60,9 @@ textarea.form-control {
   background-color: theme-color('secondary');
   border: 1px solid theme-color('secondary');
   border-right: none;
+  &.text-muted {
+    color: theme-color('light') !important;
+  }
 }
 
 .input-group input {
@@ -259,6 +262,10 @@ ul.pagination {
         background: $bgcolor-list-hover;
       }
 
+      .grw-pagetree-count {
+        background: $bgcolor-sidebar-list-group;
+      }
+
       .grw-pagetree-button {
         &:not(:hover) {
           svg {
@@ -449,3 +456,21 @@ ul.pagination {
 .grw-modal-head {
   border-color: $border-color-global;
 }
+
+/*
+ * search page
+ */
+.on-search {
+  .grw-search-result-item {
+    &.active {
+      background-color: lighten($bgcolor-global, 9%) !important;
+    }
+  }
+  .list-group-item-action:hover {
+    background-color: $bgcolor-list-hover;
+  }
+
+  .grw-search-result-list-snippet {
+    color: theme-color('light');
+  }
+}

+ 4 - 1
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -5,7 +5,6 @@ $color-list-hover: $color-global !default;
 $bgcolor-list-hover: lighten($primary, 72%) !default;
 $bgcolor-list-active: lighten($primary, 65%) !default;
 $color-list-active: color-yiq($bgcolor-list-active) !default;
-$bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !default;
 $bgcolor-table: null !default;
 $border-color-table: $gray-200 !default;
@@ -176,6 +175,10 @@ $border-color: $border-color-global;
         background: $bgcolor-list-hover;
       }
 
+      .grw-pagetree-count {
+        background: $bgcolor-sidebar-list-group;
+      }
+
       .grw-pagetree-button {
         &:not(:hover) {
           svg {

+ 16 - 12
packages/app/src/styles/theme/_apply-colors.scss

@@ -17,11 +17,11 @@ $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcol
 $color-seen-user: #549c79 !default;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
-$bordercolor-search-item-left-active: $primary;
-$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%) !default;
+$bgcolor-search-item-active: lighten($primary, 76%) !default;
 $color-search-item-pagelist-meta: $gray-500 !default;
 $color-search-page-list-title: $color-global !default;
 $color-search-page-list-snippet: $gray-600 !default;
+$bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -613,6 +613,9 @@ body.pathname-sidebar {
  * GROWI search result
  */
 .search-result {
+  .grw-search-page-nav {
+    background-color: $bgcolor-subnav;
+  }
   .search-result-list {
     .search-control {
       background-color: $bgcolor-global;
@@ -622,10 +625,10 @@ body.pathname-sidebar {
         background-color: $bgcolor-keyword-highlighted;
       }
       .page-list-ul {
-        .page-list-li {
+        .grw-search-result-item {
           &.active {
             background-color: $bgcolor-search-item-active;
-            border-color: $bordercolor-search-item-left-active;
+            border-color: $primary;
           }
         }
       }
@@ -638,11 +641,7 @@ body.pathname-sidebar {
     }
   }
 
-  .search-result-page-title {
-    color: $color-search-page-list-title;
-  }
-
-  .search-result-list-snippet {
+  .grw-search-result-list-snippet {
     color: $color-search-page-list-snippet;
   }
 }
@@ -706,9 +705,14 @@ mark.rbt-highlight-text {
 }
 
 // Page Management Dropdown icon
-.grw-btn-page-management:hover,
-.grw-btn-page-management:focus {
-  background-color: rgba($color-link, 0.15);
+.grw-btn-page-management {
+  &:hover,
+  &:focus {
+    background-color: rgba($color-link, 0.15);
+  }
+  &:active {
+    background-color: rgba($color-link, 0.2);
+  }
 }
 
 /*