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

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",
   "Update": "Update",
   "Update Page": "Update Page",
+  "Error": "Error",
   "Warning": "Warning",
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
@@ -169,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",

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

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

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

@@ -42,6 +42,7 @@
 	"Initialize": "初始化",
   "Update": "更新",
 	"Update Page": "更新本页",
+	"Error": "误差",
 	"Warning": "警告",
   "Sign in": "登录",
 	"Sign up is here": "注册",
@@ -169,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');
     }
   }
 

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

+ 31 - 22
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,12 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
     }
   };
   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>
-      </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
         <button
           type="button"
@@ -53,36 +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>}
+
+        {!isEnableActions && (
+          <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'))}>
-            <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 && (
           <>
-            <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>
   );
 
 };

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

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

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

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

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

@@ -182,6 +182,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)

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

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