soumaeda 2 лет назад
Родитель
Сommit
470649c5f3
22 измененных файлов с 209 добавлено и 94 удалено
  1. 1 1
      README.md
  2. 1 1
      README_JP.md
  3. 1 1
      apps/app/public/static/locales/en_US/admin.json
  4. 1 1
      apps/app/public/static/locales/ja_JP/admin.json
  5. 1 1
      apps/app/public/static/locales/zh_CN/admin.json
  6. 1 1
      apps/app/resource/locales/en_US/welcome.md
  7. 4 4
      apps/app/resource/locales/ja_JP/admin/userInvitation.ejs
  8. 4 4
      apps/app/resource/locales/ja_JP/admin/userResetPassword.ejs
  9. 6 6
      apps/app/resource/locales/ja_JP/admin/userWaitingActivation.ejs
  10. 1 1
      apps/app/resource/locales/ja_JP/welcome.md
  11. 1 1
      apps/app/resource/locales/zh_CN/welcome.md
  12. 1 0
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  13. 5 21
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.tsx
  14. 64 0
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx
  15. 17 20
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginsExtensionPageContents.tsx
  16. 47 0
      apps/app/src/features/growi-plugin/client/stores/admin-plugins.tsx
  17. 7 0
      apps/app/src/interfaces/search.ts
  18. 4 0
      apps/app/src/server/routes/apiv3/pages.js
  19. 14 0
      apps/app/src/server/routes/attachment.js
  20. 2 0
      apps/app/src/server/routes/tag.js
  21. 11 7
      apps/app/src/server/service/search-delegator/elasticsearch-client.ts
  22. 15 24
      apps/app/src/server/service/search-delegator/elasticsearch.ts

+ 1 - 1
README.md

@@ -37,7 +37,7 @@
 # Features
 
 - **Features**
-  - Create hierarchical pages with markdown -> [HERE](https://docs.growi.org/en/guide/getting-started/five_minutes.html) is 5 minutes tutorial
+  - Create hierarchical pages with markdown -> [Try GROWI on the demo site](https://docs.growi.org/en/guide/getting-started/try_growi.html)
   - Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
     - [GROWI Docs: HackMD(CodiMD) Integration](https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html)
   - Support Authentication with LDAP / Active Directory, OAuth

+ 1 - 1
README_JP.md

@@ -36,7 +36,7 @@
 # 機能紹介
 
 - **主な機能**
-  - マークダウンを使用してページを階層構造で作成することが可能です。 -> 5 分間チュートリアルは[こちら](https://docs.growi.org/ja/guide/getting-started/five_minutes.html)。
+  - マークダウンを使用してページを階層構造で作成することが可能です。 -> [デモサイトで GROWI を体験する](https://docs.growi.org/ja/guide/getting-started/try_growi.html)。
   - [HackMD(CodiMd)](https://hackmd.io/) と連携することで同時多人数編集が可能です。
     - [GROWI Docs: HackMD(CodiMD) 連携](https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html)
   - LDAP / Active Direcotry , OAuth 認証をサポートしています。

+ 1 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -871,7 +871,7 @@
     "plugin_card": "Plugin Card",
     "plugin_is_not_installed": "Plugin is not installed",
     "install": "Install",
-    "delete": "Delete"
+    "confirm": "Delete plugin?"
   },
   "cloud_setting_management": {
     "to_cloud_settings": "Open GROWI.cloud Settings"

+ 1 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -879,7 +879,7 @@
     "plugin_card": "プラグインカード",
     "plugin_is_not_installed": "プラグインがインストールされていません",
     "install": "インストール",
-    "delete": "削除"
+    "confirm": "プラグインを削除しますか?"
   },
   "cloud_setting_management": {
     "to_cloud_settings": "GROWI.cloud の管理画面へ"

+ 1 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -879,7 +879,7 @@
     "plugin_card": "Plugin Card",
     "plugin_is_not_installed": "Plugin is not installed",
     "install": "Install",
-    "delete": "Delete"
+    "confirm": "Delete plugin?"
   },
   "cloud_setting_management": {
     "to_cloud_settings": "進入 GROWI.cloud 的管理界面"

+ 1 - 1
apps/app/resource/locales/en_US/welcome.md

@@ -20,7 +20,7 @@ Let's increase the information exchange everyday.
 - Once we finished, press the "**Update**" button to publish the page.
     - We can also save it by `Ctrl(⌘) + S`.
 
-For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
+For more information: [Create page](https://docs.growi.org/en/guide/features/create_page.html)
 
 <div class="mt-4 card border-primary">
   <div class="card-header bg-primary text-light">

+ 4 - 4
apps/app/resource/locales/ja_JP/admin/userInvitation.ejs

@@ -1,12 +1,12 @@
-Hi, <%- email %>
+こんにちは、 <%- email %>
 
-You are invited to our Wiki, you can log in with following account:
+GROWIに招待されました!次のアカウント情報でログインしてください:
 
 Email: <%- email %>
 Password: <%- password %>
-(This password was auto generated. Update required at the first time you logging in)
+(このパスワードは自動生成されます。 初回ログイン時にアップデートが必要です。)
 
-We are waiting for you!
+あなたの参加を心よりお待ちしております。
 <%- url %>
 
 --

+ 4 - 4
apps/app/resource/locales/ja_JP/admin/userResetPassword.ejs

@@ -1,10 +1,10 @@
-Hi, <%- email %>
+こんにちは、 <%- email %>
 
-Your password has been reset by the administrator, you can log in with following account:
+あなたのパスワードは管理者によってリセットされました。次のアカウントでログインできます。:
 
 Email: <%- email %>
-New Password: <%- password %>
-(This password was auto generated. Update required at the first time you logging in)
+新しいパスワード: <%- password %>
+(このパスワードは自動生成されます。 初回ログイン時にアップデートが必要です。)
 
 --
 <%- appTitle %>

+ 6 - 6
apps/app/resource/locales/ja_JP/admin/userWaitingActivation.ejs

@@ -1,17 +1,17 @@
-Hi, <%- adminUser.name %>
+こんにちは、 <%- adminUser.name %>
 
-A user registered to <%- appTitle %>.
+<%- appTitle %> に登録されているユーザー。
 
 
 ====
-Created user:
+作成したユーザー:
 
-Name: <%- createdUser.name %>
-User Name: <%- createdUser.username %>
+名前: <%- createdUser.name %>
+ユーザーネーム: <%- createdUser.username %>
 Email: <%- createdUser.email %>
 ====
 
-Please do some action with following URL:
+次のURLでアクションを起こしてください:
 <%- url %>/admin/users
 
 

+ 1 - 1
apps/app/resource/locales/ja_JP/welcome.md

@@ -19,7 +19,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
 - 書けたら "**更新**" ボタンを押してページを公開しましょう
     - `Ctrl(⌘) + S` でも保存できます
 
-さらに詳しくはこちら: [チュートリアル#新規ページ作成](https://docs.growi.org/ja/guide/tutorial/create_page.html#新規ページ作成)
+さらに詳しくはこちら: [ページを作成する](https://docs.growi.org/ja/guide/features/create_page.html)
 
 <div class="mt-4 card border-primary">
   <div class="card-header bg-primary text-light">Tips</div>

+ 1 - 1
apps/app/resource/locales/zh_CN/welcome.md

@@ -20,7 +20,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 - 一旦我们完成了,按 "**更新**"按钮来发布页面。
     - 我们也可以通过`Ctrl(⌘) + S`来保存。
 
-了解更多信息: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
+了解更多信息: [Create page](https://docs.growi.org/en/guide/features/create_page.html)
 
 <div class="mt-4 card border-primary">
   <div class="card-header bg-primary text-light">

+ 1 - 0
apps/app/src/components/ReactMarkdownComponents/LightBox.tsx

@@ -10,6 +10,7 @@ export const LightBox = (props) => {
       <FsLightbox
         toggler={toggler}
         sources={[props.src]}
+        type="image"
       />
     </>
   );

+ 5 - 21
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginCard.tsx

@@ -3,7 +3,7 @@ import React, { useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 
-import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
 import styles from './PluginCard.module.scss';
@@ -13,14 +13,14 @@ type Props = {
   name: string,
   url: string,
   isEnalbed: boolean,
-  mutate: () => void,
   desc?: string,
+  onDelete: () => void,
 }
 
 export const PluginCard = (props: Props): JSX.Element => {
 
   const {
-    id, name, url, isEnalbed, desc, mutate,
+    id, name, url, isEnalbed, desc,
   } = props;
 
   const { t } = useTranslation('admin');
@@ -70,30 +70,14 @@ export const PluginCard = (props: Props): JSX.Element => {
 
   const PluginDeleteButton = (): JSX.Element => {
 
-    const onClickPluginDeleteBtnHandler = async() => {
-      const reqUrl = `/plugins/${id}/remove`;
-
-      try {
-        const res = await apiv3Delete(reqUrl);
-        const pluginName = res.data.pluginName;
-        toastSuccess(t('toaster.remove_plugin_success', { pluginName }));
-      }
-      catch (err) {
-        toastError(err);
-      }
-      finally {
-        mutate();
-      }
-    };
-
     return (
       <div className="">
         <button
           type="submit"
           className="btn btn-primary"
-          onClick={() => onClickPluginDeleteBtnHandler()}
+          onClick={props.onDelete}
         >
-          {t('plugins.delete')}
+          {t('Delete')}
         </button>
       </div>
     );

+ 64 - 0
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx

@@ -0,0 +1,64 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
+import {
+  Button, Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { apiv3Delete } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+
+import { useSWRxAdminPlugins, usePluginDeleteModal } from '../../../stores/admin-plugins';
+
+export const PluginDeleteModal: React.FC = () => {
+
+  const { t } = useTranslation('admin');
+  const { mutate } = useSWRxAdminPlugins();
+  const { data: pluginDeleteModalData, close: closePluginDeleteModal } = usePluginDeleteModal();
+  const isOpen = pluginDeleteModalData?.isOpen;
+  const id = pluginDeleteModalData?.id;
+  const name = pluginDeleteModalData?.name;
+  const url = pluginDeleteModalData?.url;
+
+  const toggleHandler = useCallback(() => {
+    closePluginDeleteModal();
+  }, [closePluginDeleteModal]);
+
+  const onClickDeleteButtonHandler = useCallback(async() => {
+    const reqUrl = `/plugins/${id}/remove`;
+
+    try {
+      const res = await apiv3Delete(reqUrl);
+      const pluginName = res.data.pluginName;
+      closePluginDeleteModal();
+      toastSuccess(t('toaster.remove_plugin_success', { pluginName }));
+      mutate();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [id, closePluginDeleteModal, t, mutate]);
+
+  return (
+    <Modal isOpen={isOpen} toggle={toggleHandler}>
+      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light" name={name}>
+        <span>
+          <i className="icon-fw icon-fire"></i>
+          {t('plugins.confirm')}
+        </span>
+      </ModalHeader>
+      <ModalBody>
+        <div className="card well mt-2 p-2" key={id}>
+          <Link href={`${url}`} legacyBehavior>{name}</Link>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <Button color="danger" onClick={onClickDeleteButtonHandler}>
+          <i className="icon-fw icon-fire"></i>
+          {t('Delete')}
+        </Button>
+      </ModalFooter>
+    </Modal>
+  );
+};

+ 17 - 20
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginsExtensionPageContents.tsx

@@ -1,9 +1,10 @@
 import React from 'react';
 
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { Spinner } from 'reactstrap';
 
-import { useSWRxAdminPlugins } from '../../../stores/admin-plugins';
+import { useSWRxAdminPlugins, usePluginDeleteModal } from '../../../stores/admin-plugins';
 
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
@@ -18,8 +19,10 @@ const Loading = (): JSX.Element => {
 
 export const PluginsExtensionPageContents = (): JSX.Element => {
   const { t } = useTranslation('admin');
-
+  const PluginDeleteModal = dynamic(() => import('./PluginDeleteModal')
+    .then(mod => mod.PluginDeleteModal), { ssr: false });
   const { data, mutate } = useSWRxAdminPlugins();
+  const { open: openPluginDeleteModal } = usePluginDeleteModal();
 
   return (
     <div>
@@ -45,28 +48,22 @@ export const PluginsExtensionPageContents = (): JSX.Element => {
                 { data.plugins.length === 0 && (
                   <div>{t('plugins.plugin_is_not_installed')}</div>
                 )}
-                { data.plugins.map((plugin) => {
-                  const pluginId = plugin._id;
-                  const pluginName = plugin.meta.name;
-                  const pluginUrl = plugin.origin.url;
-                  const pluginIsEnabled = plugin.isEnabled;
-                  const pluginDiscription = plugin.meta.desc;
-                  return (
-                    <PluginCard
-                      key={pluginId}
-                      id={pluginId}
-                      name={pluginName}
-                      url={pluginUrl}
-                      isEnalbed={pluginIsEnabled}
-                      desc={pluginDiscription}
-                      mutate={mutate}
-                    />
-                  );
-                })}
+                {data.plugins.map(plugin => (
+                  <PluginCard
+                    key={plugin._id}
+                    id={plugin._id}
+                    name={plugin.meta.name}
+                    url={plugin.origin.url}
+                    isEnalbed={plugin.isEnabled}
+                    desc={plugin.meta.desc}
+                    onDelete={() => openPluginDeleteModal(plugin)}
+                  />
+                ))}
               </div>
             )}
         </div>
       </div>
+      <PluginDeleteModal />
 
     </div>
   );

+ 47 - 0
apps/app/src/features/growi-plugin/client/stores/admin-plugins.tsx

@@ -1,6 +1,7 @@
 import useSWR, { SWRResponse } from 'swr';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { useStaticSWR } from '~/stores/use-static-swr';
 
 import type { IGrowiPluginHasId } from '../../interfaces';
 
@@ -22,3 +23,49 @@ export const useSWRxAdminPlugins = (): SWRResponse<Plugins, Error> => {
     },
   );
 };
+
+/*
+ * PluginDeleteModal
+ */
+type PluginDeleteModalStatus = {
+  isOpen: boolean,
+  id: string,
+  name: string,
+  url: string,
+}
+
+type PluginDeleteModalUtils = {
+  open(plugin: IGrowiPluginHasId): Promise<void>,
+  close(): Promise<void>,
+}
+
+export const usePluginDeleteModal = (): SWRResponse<PluginDeleteModalStatus, Error> & PluginDeleteModalUtils => {
+  const initialStatus: PluginDeleteModalStatus = {
+    isOpen: false,
+    id: '',
+    name: '',
+    url: '',
+  };
+
+  const swrResponse = useStaticSWR<PluginDeleteModalStatus, Error>('pluginDeleteModal', undefined, { fallbackData: initialStatus });
+  const { mutate } = swrResponse;
+
+  const open = async(plugin) => {
+    mutate({
+      isOpen: true,
+      id: plugin._id,
+      name: plugin.meta.name,
+      url: plugin.origin.url,
+    });
+  };
+
+  const close = async() => {
+    mutate(initialStatus);
+  };
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
+};

+ 7 - 0
apps/app/src/interfaces/search.ts

@@ -21,6 +21,13 @@ export type ISearchResultMeta = {
   },
 }
 
+export type ISearchResultData = {
+  _id: string
+  _score: number
+  _source: any
+  _highlight: any
+}
+
 export type ISearchResult<T> = ISearchResultMeta & {
   data: T[],
 }

+ 4 - 0
apps/app/src/server/routes/apiv3/pages.js

@@ -303,6 +303,10 @@ module.exports = (crowi) => {
     // check whether path starts slash
     path = addHeadingSlash(path);
 
+    if (!isCreatablePage(path)) {
+      return res.apiv3Err(`Could not use the path '${path}'`);
+    }
+
     if (isUserPage(path)) {
       const isExistUser = await User.isExistUserByUserPagePath(path);
       if (!isExistUser) {

+ 14 - 0
apps/app/src/server/routes/attachment.js

@@ -1,3 +1,5 @@
+import { isCreatablePage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
+
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
@@ -134,6 +136,7 @@ const ApiResponse = require('../util/apiResponse');
 module.exports = function(crowi, app) {
   const Attachment = crowi.model('Attachment');
   const Page = crowi.model('Page');
+  const User = crowi.model('User');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const { attachmentService, globalNotificationService } = crowi;
 
@@ -468,6 +471,17 @@ module.exports = function(crowi, app) {
     if (pageId == null) {
       logger.debug('Create page before file upload');
 
+      if (!isCreatablePage(pagePath)) {
+        return res.json(ApiResponse.error(`Could not use the path '${pagePath}'`));
+      }
+
+      if (isUserPage(pagePath)) {
+        const isExistUser = await User.isExistUserByUserPagePath(pagePath);
+        if (!isExistUser) {
+          return res.json(ApiResponse.error("Unable to create a page under a non-existent user's user page"));
+        }
+      }
+
       const isAclEnabled = crowi.aclService.isAclEnabled();
       const grant = isAclEnabled ? Page.GRANT_OWNER : Page.GRANT_PUBLIC;
 

+ 2 - 0
apps/app/src/server/routes/tag.js

@@ -107,6 +107,8 @@ module.exports = function(crowi, app) {
    *                properties:
    *                  pageId:
    *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                  revisionId:
+   *                    $ref: '#/components/schemas/Revision/properties/_id'
    *                  tags:
    *                    $ref: '#/components/schemas/Tags'
    *        responses:

+ 11 - 7
apps/app/src/server/service/search-delegator/elasticsearch-client.ts

@@ -53,9 +53,9 @@ export default class ElasticsearchClient {
   };
 
   cluster = {
-    health: (params: ES7RequestParams.ClusterHealth & estypes.ClusterHealthRequest)
+    health: ()
     : Promise<ES7ApiResponse<ClusterHealthResponse> | estypes.ClusterHealthResponse> =>
-      this.client instanceof ES7Client ? this.client.cluster.health(params) : this.client.cluster.health(params),
+      this.client instanceof ES7Client ? this.client.cluster.health() : this.client.cluster.health(),
   };
 
   indices = {
@@ -71,9 +71,11 @@ export default class ElasticsearchClient {
     : Promise<IndicesExistsResponse | estypes.IndicesExistsResponse> =>
       this.client instanceof ES7Client ? (await this.client.indices.exists(params)).body as IndicesExistsResponse : this.client.indices.exists(params),
 
-    existsAlias: (params: ES7RequestParams.IndicesExistsAlias & estypes.IndicesExistsAliasRequest)
-    : Promise<ES7ApiResponse<IndicesExistsAliasResponse> | estypes.IndicesExistsAliasResponse> =>
-      this.client instanceof ES7Client ? this.client.indices.existsAlias(params) : this.client.indices.existsAlias(params),
+    existsAlias: async(params: ES7RequestParams.IndicesExistsAlias & estypes.IndicesExistsAliasRequest)
+    : Promise<IndicesExistsAliasResponse | estypes.IndicesExistsAliasResponse> =>
+      this.client instanceof ES7Client
+        ? (await this.client.indices.existsAlias(params)).body as IndicesExistsAliasResponse
+        : this.client.indices.existsAlias(params),
 
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
     putAlias: (params: ES7RequestParams.IndicesPutAlias & estypes.IndicesPutAliasRequest) =>
@@ -107,8 +109,10 @@ export default class ElasticsearchClient {
     return this.client instanceof ES7Client ? this.client.ping() : this.client.ping();
   }
 
-  reindex(params: ES7RequestParams.Reindex & estypes.ReindexRequest): Promise<ES7ApiResponse<ReindexResponse> | estypes.ReindexResponse> {
-    return this.client instanceof ES7Client ? this.client.reindex(params) : this.client.reindex(params);
+  reindex(indexName: string, tmpIndexName: string): Promise<ES7ApiResponse<ReindexResponse> | estypes.ReindexResponse> {
+    return this.client instanceof ES7Client
+      ? this.client.reindex({ wait_for_completion: false, body: { source: { index: indexName }, dest: { index: tmpIndexName } } })
+      : this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
   }
 
   async search(params: ES7RequestParams.Search & estypes.SearchRequest): Promise<SearchResponse | estypes.SearchResponse> {

+ 15 - 24
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -7,7 +7,7 @@ import streamToPromise from 'stream-to-promise';
 
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import {
-  IFormattedSearchResult, ISearchResult, SORT_AXIS, SORT_ORDER,
+  ISearchResult, ISearchResultData, SORT_AXIS, SORT_ORDER,
 } from '~/interfaces/search';
 import loggerFactory from '~/utils/logger';
 
@@ -58,7 +58,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   elasticsearch: any;
 
-  client: any;
+  client: ElasticsearchClient;
 
   queries: any;
 
@@ -80,7 +80,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
     this.isElasticsearchReindexOnBoot = this.configManager.getConfig('crowi', 'app:elasticsearchReindexOnBoot');
-    this.client = null;
 
     // In Elasticsearch RegExp, we don't need to used ^ and $.
     // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-regexp-query.html#_standard_operators
@@ -184,14 +183,14 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    */
   async getInfo() {
     const info = await this.client.nodes.info();
-    if (!info._nodes || !info.nodes) {
+    if (!info != null) {
       throw new Error('There is no nodes');
     }
 
     let esVersion = 'unknown';
     const esNodeInfos = {};
 
-    for (const [nodeName, nodeInfo] of Object.entries<any>(info.nodes)) {
+    for (const [nodeName, nodeInfo] of Object.entries<any>(info)) {
       esVersion = nodeInfo.version;
 
       const filteredInfo = {
@@ -275,24 +274,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     const tmpIndexName = `${indexName}-tmp`;
 
-    const reindexRequest = this.isElasticsearchV7
-      ? {
-        waitForCompletion: false,
-        body: {
-          source: { index: indexName },
-          dest: { index: tmpIndexName },
-        },
-      }
-      : {
-        wait_for_completion: false,
-        source: { index: indexName },
-        dest: { index: tmpIndexName },
-      };
-
     try {
       // reindex to tmp index
       await this.createIndex(tmpIndexName);
-      await client.reindex(reindexRequest);
+      await client.reindex(indexName, tmpIndexName);
 
       // update alias
       await client.indices.updateAliases({
@@ -345,7 +330,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
 
     // create alias
-    const { body: isExistsAlias } = await client.indices.existsAlias({ name: aliasName, index: indexName });
+    const isExistsAlias = await client.indices.existsAlias({ name: aliasName, index: indexName });
     if (!isExistsAlias) {
       await client.indices.putAlias({
         name: aliasName,
@@ -653,7 +638,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    *   data: [ pages ...],
    * }
    */
-  async searchKeyword(query): Promise<IFormattedSearchResult> {
+  async searchKeyword(query): Promise<ISearchResult<ISearchResultData>> {
 
     // for debug
     if (process.env.NODE_ENV === 'development') {
@@ -674,10 +659,16 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     const searchResponse = await this.client.search(query);
 
+    const _total = searchResponse?.hits?.total;
+    let total = 0;
+    if (typeof _total === 'object') {
+      total = _total.value;
+    }
+
     return {
       meta: {
+        total,
         took: searchResponse.took,
-        total: searchResponse.hits.total.value,
         hitsCount: searchResponse.hits.hits.length,
       },
       data: searchResponse.hits.hits.map((elm) => {
@@ -944,7 +935,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     };
   }
 
-  async search(data: SearchableData<ESQueryTerms>, user, userGroups, option?): Promise<ISearchResult<unknown>> {
+  async search(data: SearchableData<ESQueryTerms>, user, userGroups, option?): Promise<ISearchResult<ISearchResultData>> {
     const { queryString, terms } = data;
 
     if (terms == null) {