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

Merge branch 'master' into feat/109648-

Shun Miyazawa 3 лет назад
Родитель
Сommit
73da35db5c
36 измененных файлов с 424 добавлено и 238 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 1 0
      packages/app/docker/Dockerfile
  4. 10 10
      packages/app/package.json
  5. 5 2
      packages/app/public/static/locales/en_US/admin.json
  6. 1 1
      packages/app/public/static/locales/en_US/translation.json
  7. 5 2
      packages/app/public/static/locales/ja_JP/admin.json
  8. 1 1
      packages/app/public/static/locales/ja_JP/translation.json
  9. 5 2
      packages/app/public/static/locales/zh_CN/admin.json
  10. 1 1
      packages/app/public/static/locales/zh_CN/translation.json
  11. 12 8
      packages/app/src/client/services/page-operation.ts
  12. 1 1
      packages/app/src/components/Admin/App/MailSetting.tsx
  13. 0 13
      packages/app/src/components/Admin/PluginsExtension/Loading.js
  14. 92 49
      packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx
  15. 14 33
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  16. 43 32
      packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx
  17. 3 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  18. 3 1
      packages/app/src/interfaces/plugin.ts
  19. 24 1
      packages/app/src/server/models/growi-plugin.ts
  20. 94 7
      packages/app/src/server/routes/apiv3/plugins.ts
  21. 5 5
      packages/app/src/server/routes/login.js
  22. 45 16
      packages/app/src/server/service/plugin.ts
  23. 7 4
      packages/app/src/stores/editor.tsx
  24. 26 0
      packages/app/src/stores/plugin.tsx
  25. 10 5
      packages/app/src/stores/remote-latest-page.ts
  26. 0 27
      packages/app/src/stores/useInstalledPlugins.ts
  27. 1 1
      packages/codemirror-textlint/package.json
  28. 1 1
      packages/core/package.json
  29. 1 1
      packages/hackmd/package.json
  30. 1 1
      packages/preset-themes/package.json
  31. 1 1
      packages/remark-drawio/package.json
  32. 1 1
      packages/remark-growi-directive/package.json
  33. 4 4
      packages/remark-lsx/package.json
  34. 1 1
      packages/slack/package.json
  35. 1 1
      packages/slackbot-proxy/package.json
  36. 2 2
      packages/ui/package.json

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 0
packages/app/docker/Dockerfile

@@ -125,6 +125,7 @@ RUN tar -cf packages.tar \
   packages/app/resource \
   packages/app/tmp \
   packages/app/.env.production* \
+  packages/app/next.config.js \
   packages/*/package.json \
   packages/*/dist
 

+ 10 - 10
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -65,14 +65,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.0-RC.13",
-    "@growi/core": "^6.0.0-RC.13",
-    "@growi/hackmd": "^6.0.0-RC.13",
-    "@growi/preset-themes": "^6.0.0-RC.13",
-    "@growi/remark-drawio": "^6.0.0-RC.13",
-    "@growi/remark-growi-directive": "^6.0.0-RC.13",
-    "@growi/remark-lsx": "^6.0.0-RC.13",
-    "@growi/slack": "^6.0.0-RC.13",
+    "@growi/codemirror-textlint": "^6.0.0-RC.14",
+    "@growi/core": "^6.0.0-RC.14",
+    "@growi/hackmd": "^6.0.0-RC.14",
+    "@growi/preset-themes": "^6.0.0-RC.14",
+    "@growi/remark-drawio": "^6.0.0-RC.14",
+    "@growi/remark-growi-directive": "^6.0.0-RC.14",
+    "@growi/remark-lsx": "^6.0.0-RC.14",
+    "@growi/slack": "^6.0.0-RC.14",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/web-api": "^6.2.4",
@@ -201,7 +201,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.0-RC.13",
+    "@growi/ui": "^6.0.0-RC.14",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

+ 5 - 2
packages/app/public/static/locales/en_US/admin.json

@@ -377,7 +377,6 @@
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
-    "ses_settings":"SES settings",
     "test_connection": "Test connection to mail",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
     "region": "Region",
@@ -1025,6 +1024,10 @@
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "install_plugin_success": "Succeeded to install {{pluginName}}",
+    "activate_plugin_success": "Succeeded to activating {{pluginName}}",
+    "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
+    "remove_plugin_success": "Succeeded to removing {{pluginName}}"
   }
 }

+ 1 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -650,7 +650,7 @@
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}",
     "successfully_send_email_auth":"We sent an email to {{email}}. Please click the URL in the email and complete the registration.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired.",
-    "user_already_loggedin": "You cannot create a new account when you are logged in.",
+    "user_already_logged_in": "You cannot create a new account when you are logged in.",
     "registration_closed": "You are not authorized to create a new account.",
     "Username has invalid characters": "Username has invalid characters.",
     "Username field is required": "User ID field is required.",

+ 5 - 2
packages/app/public/static/locales/ja_JP/admin.json

@@ -385,7 +385,6 @@
     "gridfs_label": "MongoDB(GridFS)",
     "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
-    "ses_settings": "SES設定",
     "test_connection": "接続テスト",
     "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
     "region": "リージョン",
@@ -1033,6 +1032,10 @@
     "remove_user_success": "{{username}}を削除しました",
     "remove_external_user_success": "{{accountId}}を削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました"
+    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "install_plugin_success": "{{pluginName}}のインストールに成功しました",
+    "activate_plugin_success": "{{pluginName}}を有効化しました",
+    "deactivate_plugin_success": "{{pluginName}}を無効化しました",
+    "remove_plugin_success": "{{pluginName}}を削除しました"
   }
 }

+ 1 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -649,7 +649,7 @@
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}",
     "successfully_send_email_auth":"{{email}} にメールを送信しました。添付されたURLをクリックし、本登録を完了させてください",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。",
-    "user_already_loggedin": "ログイン中のため、新規アカウントを作成できませんでした。",
+    "user_already_logged_in": "ログイン中のため、新規アカウントを作成できませんでした。",
     "registration_closed": "新しいアカウントを作成する権限がありません。",
     "Username has invalid characters": "ユーザー名に不正な文字が含まれています.",
     "Username field is required": "User ID は必須項目です",

+ 5 - 2
packages/app/public/static/locales/zh_CN/admin.json

@@ -385,7 +385,6 @@
     "gridfs_label": "MongoDB(GridFS)",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
-    "ses_settings": "SES设置",
     "test_connection": "测试邮件服务器连接",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
     "region": "Region",
@@ -1033,6 +1032,10 @@
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "install_plugin_success": "Succeeded to install {{pluginName}}",
+    "activate_plugin_success": "Succeeded to activating {{pluginName}}",
+    "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
+    "remove_plugin_success": "Succeeded to removing {{pluginName}}"
   }
 }

+ 1 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -654,7 +654,7 @@
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}",
     "successfully_send_email_auth":"我们向 {{email}} 发送了一封电子邮件。 请点击邮件中的网址并完成注册。",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。",
-    "user_already_loggedin": "当你登录的时候,你不能创建一个新的账户。",
+    "user_already_logged_in": "当你登录的时候,你不能创建一个新的账户。",
     "registration_closed": "你无权创建一个新的账户。",
     "Username has invalid characters": "用户名有无效字符",
     "Username field is required": "用户ID字段是必需的",

+ 12 - 8
packages/app/src/client/services/page-operation.ts

@@ -1,3 +1,5 @@
+import { useCallback } from 'react';
+
 import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
@@ -130,7 +132,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   /* eslint-enable react-hooks/rules-of-hooks */
 
-  return async function(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) {
+  return useCallback(async(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => {
     const { path, pageId, revisionId } = pageInfo;
 
     const options: OptionsToSave = Object.assign({}, optionsToSave);
@@ -172,7 +174,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     await mutateIsEnabledUnsavedWarning(new Promise(r => setTimeout(() => r(false), 10)), { optimisticData: () => false });
 
     return res;
-  };
+  }, [mutateIsEnabledUnsavedWarning]);
 };
 
 export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
@@ -183,16 +185,18 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
 
-  if (pageId == null) { return }
-
   // update swr 'currentPageId', 'currentPage', remote states
-  return async() => {
-    await mutateCurrentPageId(pageId);
-    const updatedPage = await mutateCurrentPage();
+  return useCallback(async() => {
+    if (pageId == null) { return }
 
+    // update tag before page: https://github.com/weseek/growi/pull/7158
+    // !! DO NOT CHANGE THE ORDERS OF THE MUTATIONS !! -- 12.26 yuken-t
     await mutateTagsInfo(); // get from DB
     syncTagsInfoForEditor(); // sync global state for client
 
+    await mutateCurrentPageId(pageId);
+    const updatedPage = await mutateCurrentPage();
+
     if (updatedPage == null) { return }
 
     mutateEditingMarkdown(updatedPage.revision.body);
@@ -207,7 +211,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
     };
 
     setRemoteLatestPageData(remoterevisionData);
-  };
+  }, [mutateCurrentPage, mutateCurrentPageId, mutateEditingMarkdown, mutateTagsInfo, pageId, setRemoteLatestPageData, syncTagsInfoForEditor]);
 };
 
 export const unlink = async(path: string): Promise<void> => {

+ 1 - 1
packages/app/src/components/Admin/App/MailSetting.tsx

@@ -25,7 +25,7 @@ const MailSetting = (props: Props) => {
   async function submitHandler() {
     try {
       await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings'), ns: 'commons' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);

+ 0 - 13
packages/app/src/components/Admin/PluginsExtension/Loading.js

@@ -1,13 +0,0 @@
-import {
-  Spinner,
-} from 'reactstrap';
-
-const Loading = () => {
-  return (
-    <Spinner className='d-flex justify-content-center aligh-items-center'>
-      Loading...
-    </Spinner>
-  );
-};
-
-export default Loading;

+ 92 - 49
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,26 +1,103 @@
-// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React, { useState } from 'react';
 
+import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 
-import styles from './PluginCard.module.scss';
+import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
 
+import styles from './PluginCard.module.scss';
 
 type Props = {
+  id: string,
   name: string,
   url: string,
-  description: string,
+  isEnalbed: boolean,
+  mutate: () => void,
+  desc?: string,
 }
 
 export const PluginCard = (props: Props): JSX.Element => {
+
   const {
-    name, url, description,
+    id, name, url, isEnalbed, desc, mutate,
   } = props;
-  // const [isEnabled, setIsEnabled] = useState(true);
 
-  // const checkboxHandler = useCallback(() => {
-  //   setIsEnabled(false);
-  // }, []);
+  const { t } = useTranslation('admin');
+
+  const PluginCardButton = (): JSX.Element => {
+    const [isEnabled, setState] = useState<boolean>(isEnalbed);
+
+    const onChangeHandler = async() => {
+      try {
+        if (isEnabled) {
+          const reqUrl = `/plugins/${id}/deactivate`;
+          const res = await apiv3Put(reqUrl);
+          setState(!isEnabled);
+          const pluginName = res.data.pluginName;
+          toastSuccess(t('toaster.deactivate_plugin_success', { pluginName }));
+        }
+        else {
+          const reqUrl = `/plugins/${id}/activate`;
+          const res = await apiv3Put(reqUrl);
+          setState(!isEnabled);
+          const pluginName = res.data.pluginName;
+          toastSuccess(t('toaster.activate_plugin_success', { pluginName }));
+        }
+      }
+      catch (err) {
+        toastError(err);
+      }
+    };
+
+    return (
+      <div className={`${styles.plugin_card}`}>
+        <div className="switch">
+          <label className="switch__label">
+            <input
+              type="checkbox"
+              className="switch__input"
+              onChange={() => onChangeHandler()}
+              checked={isEnabled}
+            />
+            <span className="switch__content"></span>
+            <span className="switch__circle"></span>
+          </label>
+        </div>
+      </div>
+    );
+  };
+
+  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()}
+        >
+          Delete
+        </button>
+      </div>
+    );
+  };
 
   return (
     <div className="card shadow border-0" key={name}>
@@ -30,54 +107,20 @@ export const PluginCard = (props: Props): JSX.Element => {
             <h2 className="card-title h3 border-bottom pb-2 mb-3">
               <Link href={`${url}`}>{name}</Link>
             </h2>
-            <p className="card-text text-muted">{description}</p>
+            <p className="card-text text-muted">{desc}</p>
           </div>
           <div className='col-3'>
-            <div className={`${styles.plugin_card}`}>
-              <div className="switch">
-                <label className="switch__label">
-                  <input type="checkbox" className="switch__input" checked/>
-                  <span className="switch__content"></span>
-                  <span className="switch__circle"></span>
-                </label>
-              </div>
+            <div>
+              <PluginCardButton />
+            </div>
+            <div className="mt-4">
+              <PluginDeleteButton />
             </div>
-            {/* <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">
-              <input
-                type="checkbox"
-                className="custom-control-input border-0"
-                checked={isEnabled}
-                onChange={checkboxHandler}
-              />
-              <label className="custom-control-label align-center"></label>
-            </div> */}
-            {/* <Image className="mx-auto" alt="GitHub avator image" src={owner.avatar_url} width={250} height={250} /> */}
-          </div>
-        </div>
-        <div className="row">
-          <div className="col-12 d-flex flex-wrap gap-2">
-            {/* {topics?.map((topic: string) => {
-              return (
-                <span key={`${name}-${topic}`} className="badge rounded-1 mp-bg-light-blue text-dark fw-normal">
-                  {topic}
-                </span>
-              );
-            })} */}
           </div>
         </div>
       </div>
       <div className="card-footer px-5 border-top-0 mp-bg-light-blue">
         <p className="d-flex justify-content-between align-self-center mb-0">
-          <span>
-            {/* {owner.login === 'weseek' ? <FontAwesomeIcon icon={faCircleCheck} className="me-1 text-primary" /> : <></>}
-
-            <a href={owner.html_url} target="_blank" rel="noreferrer">
-              {owner.login}
-            </a> */}
-          </span>
-          {/* <span>
-            <FontAwesomeIcon icon={faCircleArrowDown} className="me-1" /> {stargazersCount}
-          </span> */}
         </p>
       </div>
     </div>

+ 14 - 33
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,11 +1,14 @@
 import React, { useCallback } from 'react';
 
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useTranslation } from 'next-i18next';
 
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRxPlugins } from '~/stores/plugin';
 
 export const PluginInstallerForm = (): JSX.Element => {
-  // const { t } = useTranslation('admin');
+  const { mutate } = useSWRxPlugins();
+  const { t } = useTranslation('admin');
 
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
@@ -25,13 +28,17 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins', { pluginInstallerForm });
-      toastSuccess('Plugin Install Successed!');
+      const res = await apiv3Post('/plugins', { pluginInstallerForm });
+      const pluginName = res.data.pluginName;
+      toastSuccess(t('toaster.install_plugin_success', { pluginName }));
     }
     catch (e) {
       toastError(e);
     }
-  }, []);
+    finally {
+      mutate();
+    }
+  }, [mutate, t]);
 
   return (
     <form role="form" onSubmit={submitHandler}>
@@ -41,39 +48,13 @@ export const PluginInstallerForm = (): JSX.Element => {
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
             name="pluginInstallerForm[url]"
-            placeholder="https://github.com/weseek/growi-plugin-lsx"
+            placeholder="https://github.com/growi/plugins"
             required
           />
           <p className="form-text text-muted">You can install plugins by inputting the GitHub URL.</p>
-          {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
         </div>
       </div>
-      {/* <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">branch</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghBranch]"
-            placeholder="main"
-          />
-          <p className="form-text text-muted">branch name</p>
-        </div>
-      </div>
-      <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">tag</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghTag]"
-            placeholder="tags"
-          />
-          <p className="form-text text-muted">tag name</p>
-        </div>
-      </div> */}
 
       <div className="row my-3">
         <div className="mx-auto">

+ 43 - 32
packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx

@@ -1,25 +1,25 @@
 import React from 'react';
 
-import type { SearchResultItem } from '~/interfaces/github-api';
-import { useInstalledPlugins } from '~/stores/useInstalledPlugins';
+import { Spinner } from 'reactstrap';
+
+import { useSWRxPlugins } from '~/stores/plugin';
 
-import Loading from './Loading';
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
 
-
-// TODO: i18n
+const Loading = (): JSX.Element => {
+  return (
+    <Spinner className='d-flex justify-content-center aligh-items-center'>
+      Loading...
+    </Spinner>
+  );
+};
 
 export const PluginsExtensionPageContents = (): JSX.Element => {
-  // const { data, error } = useInstalledPlugins();
-
-  // if (data == null) {
-  //   return <Loading />;
-  // }
+  const { data, mutate } = useSWRxPlugins();
 
   return (
     <div>
-
       <div className="row mb-5">
         <div className="col-lg-12">
           <h2 className="admin-setting-header">Plugin Installer</h2>
@@ -29,27 +29,38 @@ export const PluginsExtensionPageContents = (): JSX.Element => {
 
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">Plugins</h2>
-          <div className="d-grid gap-5">
-            <PluginCard
-              name={'growi-plugin-templates-for-office'}
-              url={'https://github.com/weseek/growi-plugin-templates-for-office'}
-              description={'GROWI markdown templates for office.'}
-            />
-            {/* <PluginCard
-              name={'growi-plugin-theme-welcome-to-fumiya-room'}
-              url={'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room'}
-              description={'Welcome to fumiya\'s room! This is very very "latest" design...'}
-            /> */}
-            <PluginCard
-              name={'growi-plugin-copy-code-to-clipboard'}
-              url={'https://github.com/weseek/growi-plugin-copy-code-to-clipboard'}
-              description={'Add copy button on code blocks.'}
-            />
-            {/* {data?.items.map((item: SearchResultItem) => {
-              return <PluginCard key={item.name} {...item} />;
-            })} */}
-          </div>
+          <h2 className="admin-setting-header">Plugins
+            <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+              <i className="icon icon-reload"></i>
+            </button>
+          </h2>
+          {data?.plugins == null
+            ? <Loading />
+            : (
+              <div className="d-grid gap-5">
+                { data.plugins.length === 0 && (
+                  <div>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}
+                    />
+                  );
+                })}
+              </div>
+            )}
         </div>
       </div>
 

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

@@ -1182,6 +1182,8 @@ CodeMirrorEditor.defaultProps = {
   lineNumbers: true,
 };
 
+const CodeMirrorEditorMemoized = memo(CodeMirrorEditor);
+
 
 const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   const { open: openDrawioModal } = useDrawioModal();
@@ -1196,7 +1198,7 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   }, [openHandsontableModal]);
 
   return (
-    <CodeMirrorEditor
+    <CodeMirrorEditorMemoized
       ref={ref}
       onClickDrawioBtn={openDrawioModalHandler}
       onClickTableBtn={openTableModalHandler}

+ 3 - 1
packages/app/src/interfaces/plugin.ts

@@ -1,4 +1,4 @@
-import { GrowiThemeMetadata } from '@growi/core';
+import { GrowiThemeMetadata, HasObjectId } from '@growi/core';
 
 export const GrowiPluginResourceType = {
   Template: 'template',
@@ -31,3 +31,5 @@ export type GrowiPluginMeta = {
 export type GrowiThemePluginMeta = GrowiPluginMeta & {
   themes: GrowiThemeMetadata[]
 }
+
+export type GrowiPluginHasId = GrowiPlugin & HasObjectId;

+ 24 - 1
packages/app/src/server/models/growi-plugin.ts

@@ -1,6 +1,6 @@
 import { GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import {
-  Schema, Model, Document,
+  Schema, Model, Document, Types,
 } from 'mongoose';
 
 import {
@@ -14,6 +14,8 @@ export interface GrowiPluginDocument extends GrowiPlugin, Document {
 export interface GrowiPluginModel extends Model<GrowiPluginDocument> {
   findEnabledPlugins(): Promise<GrowiPlugin[]>
   findEnabledPluginsIncludingAnyTypes(includingTypes: GrowiPluginResourceType[]): Promise<GrowiPlugin[]>
+  activatePlugin(id: Types.ObjectId): Promise<string>
+  deactivatePlugin(id: Types.ObjectId): Promise<string>
 }
 
 const growiThemeMetadataSchema = new Schema<GrowiThemeMetadata>({
@@ -58,6 +60,7 @@ const growiPluginSchema = new Schema<GrowiPluginDocument, GrowiPluginModel>({
 growiPluginSchema.statics.findEnabledPlugins = async function(): Promise<GrowiPlugin[]> {
   return this.find({ isEnabled: true });
 };
+
 growiPluginSchema.statics.findEnabledPluginsIncludingAnyTypes = async function(types: GrowiPluginResourceType[]): Promise<GrowiPlugin[]> {
   return this.find({
     isEnabled: true,
@@ -65,4 +68,24 @@ growiPluginSchema.statics.findEnabledPluginsIncludingAnyTypes = async function(t
   });
 };
 
+growiPluginSchema.statics.activatePlugin = async function(id: Types.ObjectId): Promise<string> {
+  const growiPlugin = await this.findOneAndUpdate({ _id: id }, { isEnabled: true });
+  if (growiPlugin == null) {
+    const message = 'No plugin found for this ID.';
+    throw new Error(message);
+  }
+  const pluginName = growiPlugin.meta.name;
+  return pluginName;
+};
+
+growiPluginSchema.statics.deactivatePlugin = async function(id: Types.ObjectId): Promise<string> {
+  const growiPlugin = await this.findOneAndUpdate({ _id: id }, { isEnabled: false });
+  if (growiPlugin == null) {
+    const message = 'No plugin found for this ID.';
+    throw new Error(message);
+  }
+  const pluginName = growiPlugin.meta.name;
+  return pluginName;
+};
+
 export default getOrCreateModel<GrowiPluginDocument, GrowiPluginModel>('GrowiPlugin', growiPluginSchema);

+ 94 - 7
packages/app/src/server/routes/apiv3/plugins.ts

@@ -1,26 +1,113 @@
-import express, { Request } from 'express';
+import express, { Request, Router } from 'express';
+import { body, query } from 'express-validator';
+import mongoose from 'mongoose';
 
 import Crowi from '../../crowi';
+import type { GrowiPluginModel } from '../../models/growi-plugin';
 
 import { ApiV3Response } from './interfaces/apiv3-response';
 
-type PluginInstallerFormRequest = Request & { form: any };
+const ObjectID = mongoose.Types.ObjectId;
+
+/*
+ * Validators
+ */
+const validator = {
+  pluginIdisRequired: [
+    query('id').isMongoId().withMessage('pluginId is required'),
+  ],
+  pluginFormValueisRequired: [
+    body('pluginInstallerForm').isString().withMessage('pluginFormValue is required'),
+  ],
+};
+
+module.exports = (crowi: Crowi): Router => {
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const adminRequired = require('../../middlewares/admin-required')(crowi);
 
-module.exports = (crowi: Crowi) => {
   const router = express.Router();
   const { pluginService } = crowi;
 
-  router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
+  router.get('/', loginRequiredStrictly, adminRequired, async(req: Request, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err('\'pluginService\' is not set up', 500);
+    }
+
+    try {
+      const GrowiPluginModel = mongoose.model('GrowiPlugin') as GrowiPluginModel;
+      const data = await GrowiPluginModel.find({});
+      return res.apiv3({ plugins: data });
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+  });
+
+  router.post('/', loginRequiredStrictly, adminRequired, validator.pluginFormValueisRequired, async(req: Request, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err('\'pluginService\' is not set up', 500);
+    }
+
+    const { pluginInstallerForm: formValue } = req.body;
+
+    try {
+      const pluginName = await pluginService.install(formValue);
+      return res.apiv3({ pluginName });
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+  });
+
+  router.put('/:id/activate', loginRequiredStrictly, adminRequired, validator.pluginIdisRequired, async(req: Request, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err('\'pluginService\' is not set up', 500);
+    }
+    const { id } = req.params;
+    const pluginId = new ObjectID(id);
+
+    try {
+      const GrowiPluginModel = mongoose.model('GrowiPlugin') as GrowiPluginModel;
+      const pluginName = await GrowiPluginModel.activatePlugin(pluginId);
+      return res.apiv3({ pluginName });
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+  });
+
+  router.put('/:id/deactivate', loginRequiredStrictly, adminRequired, validator.pluginIdisRequired, async(req: Request, res: ApiV3Response) => {
     if (pluginService == null) {
       return res.apiv3Err('\'pluginService\' is not set up', 500);
     }
 
+    const { id } = req.params;
+    const pluginId = new ObjectID(id);
+
+    try {
+      const GrowiPluginModel = mongoose.model('GrowiPlugin') as GrowiPluginModel;
+      const pluginName = await GrowiPluginModel.deactivatePlugin(pluginId);
+      return res.apiv3({ pluginName });
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+  });
+
+  router.delete('/:id/remove', loginRequiredStrictly, adminRequired, validator.pluginIdisRequired, async(req: Request, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err('\'pluginService\' is not set up', 500);
+    }
+
+    const { id } = req.params;
+    const pluginId = new ObjectID(id);
+
     try {
-      await pluginService.install(req.body.pluginInstallerForm);
-      return res.apiv3({});
+      const pluginName = await pluginService.deletePlugin(pluginId);
+      return res.apiv3({ pluginName });
     }
     catch (err) {
-      return res.apiv3Err(err, 400);
+      return res.apiv3Err(err);
     }
   });
 

+ 5 - 5
packages/app/src/server/routes/login.js

@@ -102,12 +102,12 @@ module.exports = function(crowi, app) {
 
   actions.register = function(req, res) {
     if (req.user != null) {
-      return res.apiv3Err('user_already_logged_in', 403);
+      return res.apiv3Err('message.user_already_logged_in', 403);
     }
 
     // config で closed ならさよなら
     if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
-      return res.apiv3Err('registration_closed', 403);
+      return res.apiv3Err('message.registration_closed', 403);
     }
 
     if (!req.form.isValid) {
@@ -145,17 +145,17 @@ module.exports = function(crowi, app) {
       const isMailerSetup = mailService.isMailerSetup ?? false;
 
       if (!isMailerSetup && registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-        return res.apiv3Err(['email_settings_is_not_setup'], 403);
+        return res.apiv3Err(['message.email_settings_is_not_setup'], 403);
       }
 
       User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
         if (err) {
           const errors = [];
           if (err.name === 'UserUpperLimitException') {
-            errors.push('can_not_register_maximum_number_of_users');
+            errors.push('message.can_not_register_maximum_number_of_users');
           }
           else {
-            errors.push('failed_to_register');
+            errors.push('message.failed_to_register');
           }
           return res.apiv3Err(errors, 405);
         }

+ 45 - 16
packages/app/src/server/service/plugin.ts

@@ -34,7 +34,7 @@ function retrievePluginManifest(growiPlugin: GrowiPlugin): ViteManifest {
 }
 
 export interface IPluginService {
-  install(origin: GrowiPluginOrigin): Promise<void>
+  install(origin: GrowiPluginOrigin): Promise<string>
   retrieveThemeHref(theme: string): Promise<string | undefined>
   retrieveAllPluginResourceEntries(): Promise<GrowiPluginResourceEntries>
   downloadNotExistPluginRepositories(): Promise<void>
@@ -62,7 +62,7 @@ export class PluginService implements IPluginService {
           const ghBranch = 'main';
           const match = ghPathname.match(githubReposIdPattern);
           if (ghUrl.hostname !== 'github.com' || match == null) {
-            throw new Error('The GitHub Repository URL is invalid.');
+            throw new Error('GitHub repository URL is invalid.');
           }
 
           const ghOrganizationName = match[1];
@@ -79,7 +79,7 @@ export class PluginService implements IPluginService {
     }
   }
 
-  async install(origin: GrowiPluginOrigin): Promise<void> {
+  async install(origin: GrowiPluginOrigin): Promise<string> {
     try {
     // download
       const ghUrl = new URL(origin.url);
@@ -89,7 +89,7 @@ export class PluginService implements IPluginService {
 
       const match = ghPathname.match(githubReposIdPattern);
       if (ghUrl.hostname !== 'github.com' || match == null) {
-        throw new Error('The GitHub Repository URL is invalid.');
+        throw new Error('GitHub repository URL is invalid.');
       }
 
       const ghOrganizationName = match[1];
@@ -105,13 +105,13 @@ export class PluginService implements IPluginService {
       // save plugin metadata
       const plugins = await PluginService.detectPlugins(origin, installedPath);
       await this.savePluginMetaData(plugins);
+
+      return plugins[0].meta.name;
     }
     catch (err) {
       logger.error(err);
       throw err;
     }
-
-    return;
   }
 
   private async deleteOldPluginDocument(path: string): Promise<void> {
@@ -144,8 +144,8 @@ export class PluginService implements IPluginService {
             else {
               rejects(res.status);
             }
-          }).catch((e) => {
-            logger.error(e);
+          }).catch((err) => {
+            logger.error(err);
             // eslint-disable-next-line prefer-promise-reject-errors
             rejects('Filed to download file.');
           });
@@ -180,14 +180,9 @@ export class PluginService implements IPluginService {
       }
     };
 
-    try {
-      await downloadFile(requestUrl, zipFilePath);
-      await unzip(zipFilePath, unzippedPath);
-      await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
-    }
-    catch (err) {
-      throw err;
-    }
+    await downloadFile(requestUrl, zipFilePath);
+    await unzip(zipFilePath, unzippedPath);
+    await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
 
     return;
   }
@@ -255,6 +250,40 @@ export class PluginService implements IPluginService {
     return [];
   }
 
+  /**
+   * Delete plugin
+   */
+  async deletePlugin(pluginId: mongoose.Types.ObjectId): Promise<string> {
+    const deleteFolder = (path: fs.PathLike): Promise<void> => {
+      return fs.promises.rm(path, { recursive: true });
+    };
+
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.findById(pluginId);
+
+    if (growiPlugins == null) {
+      throw new Error('No plugin found for this ID.');
+    }
+
+    try {
+      const growiPluginsPath = path.join(pluginStoringPath, growiPlugins.installedPath);
+      await deleteFolder(growiPluginsPath);
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Filed to delete plugin repository.');
+    }
+
+    try {
+      await GrowiPlugin.deleteOne({ _id: pluginId });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Filed to delete plugin from GrowiPlugin documents.');
+    }
+
+    return growiPlugins.meta.name;
+  }
 
   async retrieveThemeHref(theme: string): Promise<string | undefined> {
 

+ 7 - 4
packages/app/src/stores/editor.tsx

@@ -1,3 +1,5 @@
+import { useCallback } from 'react';
+
 import { Nullable, withUtils, SWRResponseWithUtils } from '@growi/core';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
@@ -109,13 +111,14 @@ export type IPageTagsForEditorsOption = {
 export const usePageTagsForEditors = (pageId: Nullable<string>): SWRResponse<string[], Error> & IPageTagsForEditorsOption => {
   const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
   const swrResult = useStaticSWR<string[], Error>('pageTags', undefined);
+  const { mutate } = swrResult;
+  const sync = useCallback((): void => {
+    mutate(tagsInfoData?.tags || [], false);
+  }, [mutate, tagsInfoData?.tags]);
 
   return {
     ...swrResult,
-    sync: (): void => {
-      const { mutate } = swrResult;
-      mutate(tagsInfoData?.tags || [], false);
-    },
+    sync,
   };
 };
 

+ 26 - 0
packages/app/src/stores/plugin.tsx

@@ -0,0 +1,26 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { GrowiPluginHasId } from '~/interfaces/plugin';
+
+type Plugins = {
+  plugins: GrowiPluginHasId[]
+}
+
+const pluginsFetcher = () => {
+  return async() => {
+    const reqUrl = '/plugins';
+
+    try {
+      const res = await apiv3Get(reqUrl);
+      return res.data;
+    }
+    catch (err) {
+      throw new Error(err);
+    }
+  };
+};
+
+export const useSWRxPlugins = (): SWRResponse<Plugins, Error> => {
+  return useSWR('/plugins', pluginsFetcher());
+};

+ 10 - 5
packages/app/src/stores/remote-latest-page.ts

@@ -1,3 +1,5 @@
+import { useMemo, useCallback } from 'react';
+
 import { SWRResponse } from 'swr';
 
 import { IUser } from '~/interfaces/user';
@@ -41,7 +43,7 @@ export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageDa
   const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
 
-  const setRemoteLatestPageData = (remoteRevisionData: RemoteRevisionData) => {
+  const setRemoteLatestPageData = useCallback((remoteRevisionData: RemoteRevisionData) => {
     const {
       remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt, revisionIdHackmdSynced, hasDraftOnHackmd,
     } = remoteRevisionData;
@@ -51,10 +53,13 @@ export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageDa
     mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
     mutateRevisionIdHackmdSynced(revisionIdHackmdSynced);
     mutateHasDraftOnHackmd(hasDraftOnHackmd);
-  };
+  // eslint-disable-next-line max-len
+  }, [mutateHasDraftOnHackmd, mutateRemoteRevisionBody, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser, mutateRemoteRevisionLastUpdatedAt, mutateRevisionIdHackmdSynced]);
 
-  return {
-    setRemoteLatestPageData,
-  };
+  return useMemo(() => {
+    return {
+      setRemoteLatestPageData,
+    };
+  }, [setRemoteLatestPageData]);
 
 };

+ 0 - 27
packages/app/src/stores/useInstalledPlugins.ts

@@ -1,27 +0,0 @@
-import useSWR, { SWRResponse } from 'swr';
-
-import type { SearchResult, SearchResultItem } from '../interfaces/github-api';
-
-const pluginFetcher = (owner: string, repo: string) => {
-  return async() => {
-    const reqUrl = `/api/fetch_repository?owner=${owner}&repo=${repo}`;
-    const data = await fetch(reqUrl).then(res => res.json());
-    return data.searchResultItem;
-  };
-};
-
-export const useInstalledPlugin = (owner: string, repo: string): SWRResponse<SearchResultItem | null, Error> => {
-  return useSWR(`${owner}/{repo}`, pluginFetcher(owner, repo));
-};
-
-const pluginsFetcher = () => {
-  return async() => {
-    const reqUrl = '/api/fetch_repositories';
-    const data = await fetch(reqUrl).then(res => res.json());
-    return data.searchResult;
-  };
-};
-
-export const useInstalledPlugins = (): SWRResponse<SearchResult | null, Error> => {
-  return useSWR('/api/fetch_repositories', pluginsFetcher());
-};

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "main": "dist/index.js",

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "files": [

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 4 - 4
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -23,9 +23,9 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^6.0.0-RC.13",
-    "@growi/remark-growi-directive": "^6.0.0-RC.13",
-    "@growi/ui": "^6.0.0-RC.13",
+    "@growi/core": "^6.0.0-RC.14",
+    "@growi/remark-growi-directive": "^6.0.0-RC.14",
+    "@growi/ui": "^6.0.0-RC.14",
     "swr": "^1.3.0"
   },
   "devDependencies": {

+ 1 - 1
packages/slack/package.json

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

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

@@ -26,7 +26,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.0-RC.13",
+    "@growi/slack": "^6.0.0-RC.14",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.0-RC.14",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
   },
   "dependencies": {
-    "@growi/core": "^6.0.0-RC.13"
+    "@growi/core": "^6.0.0-RC.14"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",