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

Merge branch 'dev/5.0.x' into fix/83698-dot-button-design

* dev/5.0.x: (26 commits)
  remove unused state
  change toastError position
  fix try catch syntax
  improve fetch method
  use swr data
  improve class
  using useTranslation instead of withTranslation
  replaced class component to functional one
  fix grammar
  clean code
  fix alert message
  add z-index
  remove unnecessary styles
  set preventOverflow to modifiers of DropdownMenu
  change to reactstrap
  Improved alert message
  Added a comment
  Improved regex
  escape tags
  Mode style
  ...
Mao 4 лет назад
Родитель
Сommit
4d1caafdfc

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

@@ -40,6 +40,7 @@
   "account_id": "Account Id",
   "account_id": "Account Id",
   "Update": "Update",
   "Update": "Update",
   "Update Page": "Update Page",
   "Update Page": "Update Page",
+  "Error": "Error",
   "Warning": "Warning",
   "Warning": "Warning",
   "Sign in": "Sign in",
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
   "Sign up is here": "Sign up",
@@ -169,7 +170,8 @@
   "form_validation": {
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
     "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": {
   "not_found_page": {
     "Create Page": "Create Page",
     "Create Page": "Create Page",

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

@@ -41,6 +41,7 @@
   "Initialize": "初期化",
   "Initialize": "初期化",
   "Update": "更新",
   "Update": "更新",
   "Update Page": "ページを更新",
   "Update Page": "ページを更新",
+  "Error": "エラー",
   "Warning": "注意",
   "Warning": "注意",
   "Sign in": "ログイン",
   "Sign in": "ログイン",
   "Sign up is here": "新規登録はこちら",
   "Sign up is here": "新規登録はこちら",
@@ -171,7 +172,8 @@
   "form_validation": {
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
     "required": "%sに値を入力してください",
-    "invalid_syntax": "%sの構文が不正です"
+    "invalid_syntax": "%sの構文が不正です",
+    "title_required": "タイトルを入力してください"
   },
   },
   "not_found_page": {
   "not_found_page": {
     "Create Page": "ページを作成する",
     "Create Page": "ページを作成する",

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

@@ -42,6 +42,7 @@
 	"Initialize": "初始化",
 	"Initialize": "初始化",
   "Update": "更新",
   "Update": "更新",
 	"Update Page": "更新本页",
 	"Update Page": "更新本页",
+	"Error": "误差",
 	"Warning": "警告",
 	"Warning": "警告",
   "Sign in": "登录",
   "Sign in": "登录",
 	"Sign up is here": "注册",
 	"Sign up is here": "注册",
@@ -169,7 +170,8 @@
 	"form_validation": {
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
 		"required": "%s 是必需的",
-		"invalid_syntax": "%s的语法无效。"
+		"invalid_syntax": "%s的语法无效。",
+    "title_required": "标题是必需的。"
   },
   },
   "not_found_page": {
   "not_found_page": {
     "Create 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.timer = null;
 
 
     this.state = {
     this.state = {
-      retrieveError: null,
       growiVersion: '',
       growiVersion: '',
       nodeVersion: '',
       nodeVersion: '',
       npmVersion: '',
       npmVersion: '',
@@ -69,7 +68,7 @@ export default class AdminHomeContainer extends Container {
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      toastError(new Error('Failed to fetch data'));
+      throw new Error('Failed to retrive AdminHome data');
     }
     }
   }
   }
 
 

+ 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 PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
 import { Tooltip } from 'reactstrap';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -10,117 +10,112 @@ import { toastError } from '~/client/util/apiNotification';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import SystemInfomationTable from './SystemInfomationTable';
 import SystemInfomationTable from './SystemInfomationTable';
 import InstalledPluginTable from './InstalledPluginTable';
 import InstalledPluginTable from './InstalledPluginTable';
 import EnvVarsTable from './EnvVarsTable';
 import EnvVarsTable from './EnvVarsTable';
 
 
 const logger = loggerFactory('growi:admin');
 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 {
     try {
       await adminHomeContainer.retrieveAdminHomeData();
       await adminHomeContainer.retrieveAdminHomeData();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      adminHomeContainer.setState({ retrieveError: err });
       logger.error(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>
           </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>
 
 
-        <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>
-
-        <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>
-
-        <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>
         </div>
         </div>
-      </Fragment>
-    );
-  }
+      </div>
+    </>
+  );
+};
 
 
-}
 
 
 const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AppContainer, AdminHomeContainer]);
 const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AppContainer, AdminHomeContainer]);
 
 
 AdminHome.propTypes = {
 AdminHome.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).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, {
 import React, {
   FC, memo, useEffect, useRef, useState,
   FC, memo, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
+import { useTranslation } from 'react-i18next';
 
 
 export const AlertType = {
 export const AlertType = {
   WARNING: 'warning',
   WARNING: 'warning',
@@ -23,6 +24,7 @@ type ClosableTextInputProps = {
 }
 }
 
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
+  const { t } = useTranslation();
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
 
 
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(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 alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
     const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
     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 (
     return (
-      <p className="text-danger text-center mt-1">{alertType}: {alertMessage}</p>
+      <p className={`${alertTextStyle} text-center mt-1`}>{t(translation)}: {alertMessage}</p>
     );
     );
   };
   };
 
 

+ 31 - 22
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

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

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

@@ -118,7 +118,7 @@ const CopyDropdown = (props) => {
           <span id={dropdownToggleId}>{children}</span>
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>
         </DropdownToggle>
 
 
-        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: null } }}>
+        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
 
 
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <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
    * @param {string} keywords
    */
    */
   getHighlightedBody(body, 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 === '') {
       if (keyword === '') {
         return;
         return;
       }
       }
       const k = keyword
       const k = keyword
-        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
         .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
         .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() {
   async renderHtml() {

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

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

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

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

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

@@ -86,8 +86,9 @@ const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
 const ItemCount: FC = () => {
 const ItemCount: FC = () => {
   return (
   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 */}
         {/* TODO: consider to show the number of children pages */}
+        00
       </span>
       </span>
     </>
     </>
   );
   );
@@ -139,8 +140,8 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const inputValidator = (title: string | null): AlertInfo | null => {
   const inputValidator = (title: string | null): AlertInfo | null => {
     if (title == null || title === '') {
     if (title == null || title === '') {
       return {
       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>
           </div>
         </button>
         </button>
         <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
         <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>
         </a>
         <div className="grw-pagetree-count-wrapper">
         <div className="grw-pagetree-count-wrapper">
           <ItemCount />
           <ItemCount />

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

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

+ 1 - 0
packages/app/src/styles/_search.scss

@@ -182,6 +182,7 @@
   .search-result-list {
   .search-result-list {
     position: sticky;
     position: sticky;
     top: 0px;
     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 {
     .search-result-list-scroll {
       // subtract the height of GrowiNavbar + (SearchControl component + other factors)
       // subtract the height of GrowiNavbar + (SearchControl component + other factors)

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

@@ -259,6 +259,10 @@ ul.pagination {
         background: $bgcolor-list-hover;
         background: $bgcolor-list-hover;
       }
       }
 
 
+      .grw-pagetree-count {
+        background: $bgcolor-sidebar-list-group;
+      }
+
       .grw-pagetree-button {
       .grw-pagetree-button {
         &:not(:hover) {
         &:not(:hover) {
           svg {
           svg {

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

@@ -176,6 +176,10 @@ $border-color: $border-color-global;
         background: $bgcolor-list-hover;
         background: $bgcolor-list-hover;
       }
       }
 
 
+      .grw-pagetree-count {
+        background: $bgcolor-sidebar-list-group;
+      }
+
       .grw-pagetree-button {
       .grw-pagetree-button {
         &:not(:hover) {
         &:not(:hover) {
           svg {
           svg {