Преглед изворни кода

Merge branch 'dev/7.0.x' into support/142719-update-login-styles

Yuki Takei пре 2 година
родитељ
комит
f020571b3c
35 измењених фајлова са 369 додато и 299 уклоњено
  1. 0 6
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  2. 0 1
      apps/app/next-env.d.ts
  3. 10 10
      apps/app/resource/locales/en_US/sandbox-bootstrap5.md
  4. 1 1
      apps/app/resource/locales/en_US/welcome.md
  5. 2 2
      apps/app/resource/locales/ja_JP/sandbox.md
  6. 1 1
      apps/app/resource/locales/ja_JP/welcome.md
  7. 40 24
      apps/app/src/components/Admin/SlackIntegration/BotTypeCard.tsx
  8. 19 23
      apps/app/src/components/Admin/SlackIntegration/Bridge.tsx
  9. 0 57
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.jsx
  10. 59 0
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.tsx
  11. 2 2
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  12. 0 57
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.jsx
  13. 57 0
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.tsx
  14. 1 1
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  15. 1 1
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  16. 21 19
      apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx
  17. 2 2
      apps/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  18. 1 2
      apps/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx
  19. 12 12
      apps/app/src/components/Admin/SlackIntegration/SlackIntegration.tsx
  20. 1 1
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  21. 4 0
      apps/app/src/components/Admin/SlackIntegration/slack-integration-util.ts
  22. 0 20
      apps/app/src/components/Admin/SlackIntegration/slak-integration-util.js
  23. 8 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  24. 9 8
      apps/app/src/components/Layout/Admin.module.scss
  25. 8 7
      apps/app/src/components/PageControls/PageControls.tsx
  26. 1 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss
  27. 6 2
      apps/app/src/components/PageEditor/PageEditorReadOnly.tsx
  28. 26 8
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  29. 5 4
      apps/app/src/pages/admin/slack-integration.page.tsx
  30. 7 0
      apps/app/src/server/crowi/index.js
  31. 9 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  32. 3 6
      apps/app/src/server/service/socket-io.js
  33. 35 5
      apps/app/src/server/service/yjs-connection-manager.ts
  34. 17 15
      packages/editor/src/services/paste-util/paste-markdown-util.ts
  35. 1 0
      packages/slack/src/interfaces/index.ts

+ 0 - 6
apps/app/_obsolete/src/styles/theme/apply-colors.scss

@@ -402,12 +402,6 @@ ul.pagination {
   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
 }
 }
 
 
-.admin-bot-card {
-  .grw-botcard-title-active {
-    color: $gray-200;
-  }
-}
-
 /*
 /*
  * Form Slider
  * Form Slider
  */
  */

+ 0 - 1
apps/app/next-env.d.ts

@@ -1,6 +1,5 @@
 /// <reference types="next" />
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
 /// <reference types="next/image-types/global" />
-/// <reference types="next/navigation-types/compat/navigation" />
 
 
 // NOTE: This file should not be edited
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/basic-features/typescript for more information.
 // see https://nextjs.org/docs/basic-features/typescript for more information.

+ 10 - 10
apps/app/resource/locales/en_US/sandbox-bootstrap5.md

@@ -120,19 +120,19 @@
 
 
 
 
 # 4. Colors
 # 4. Colors
-## 背景颜色
+## Contextual colors
 <p class="text-primary">Look, I'm in a well!</p>
 <p class="text-primary">Look, I'm in a well!</p>
 <p class="text-warning">Look, I'm in a well!</p>
 <p class="text-warning">Look, I'm in a well!</p>
 <p class="text-danger">Look, I'm in a well!</p>
 <p class="text-danger">Look, I'm in a well!</p>
 
 
-## 背景情况
+## Contextual backgrounds
 <p class="text-danger bg-primary">Look, I'm in a well!</p>
 <p class="text-danger bg-primary">Look, I'm in a well!</p>
 <p class="text-primary bg-warning">Look, I'm in a well!</p>
 <p class="text-primary bg-warning">Look, I'm in a well!</p>
 <p class="text-warning bg-danger">Look, I'm in a well!</p>
 <p class="text-warning bg-danger">Look, I'm in a well!</p>
 
 
 
 
 # 5. Collapse
 # 5. Collapse
-## 显示内容
+## Displaying content
 <a class="btn btn-primary text-white" data-bs-toggle="collapse" href="#collapse-1">
 <a class="btn btn-primary text-white" data-bs-toggle="collapse" href="#collapse-1">
   Show content
   Show content
 </a>
 </a>
@@ -146,7 +146,7 @@
   </div>
   </div>
 </div>
 </div>
 
 
-## 隐藏内容
+## Hiding content
 <a class="btn btn-secondary text-white" data-bs-toggle="collapse" href="#collapse-2">
 <a class="btn btn-secondary text-white" data-bs-toggle="collapse" href="#collapse-2">
   Hide content
   Hide content
 </a>
 </a>
@@ -161,9 +161,9 @@
 </div>
 </div>
 
 
 
 
-# 官方文件
-- [点击此处了解徽章详情](https://getbootstrap.com/docs/5.3/components/badge/)
-- [单击此处了解警报详情](https://getbootstrap.com/docs/5.3/components/alerts/)
-- [点击此处了解贺卡详情](https://getbootstrap.com/docs/5.3/components/card/)
-- [点击此处了解颜色详情](https://getbootstrap.com/docs/5.3/utilities/colors/)
-- [点击此处查看折叠详情](https://getbootstrap.com/docs/5.3/components/collapse/)
+# Official docs
+- [Click here for Badges details](https://getbootstrap.jp/docs/5.3/components/badge/)
+- [Click here for Alerts details](https://getbootstrap.jp/docs/5.3/components/alerts/)
+- [Click here for Cards details](https://getbootstrap.jp/docs/5.3/components/card/)
+- [Click here for Colors details](https://getbootstrap.jp/docs/5.3/utilities/colors/)
+- [Click here for Collapse details](https://getbootstrap.jp/docs/5.3/components/collapse/)

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

@@ -33,7 +33,7 @@ Let's increase the amount of information shared on a daily base!
 - GROWI also allows pages to be viewed by users outside the company who do not have an account
 - GROWI also allows pages to be viewed by users outside the company who do not have an account
     - Let's share information with users outside your company using shared links!
     - Let's share information with users outside your company using shared links!
 
 
-#### :bulb: Check [Sndbox](/Sandbox) to learn more on how to edit pages!
+#### :bulb: Check [Sandbox](/Sandbox) to learn more on how to edit pages!
 
 
 
 
 # :wrench: For Administrators - Once GROWI is created
 # :wrench: For Administrators - Once GROWI is created

+ 2 - 2
apps/app/resource/locales/ja_JP/sandbox.md

@@ -166,7 +166,7 @@
 - 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます
 - 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます
 
 
 #### 活用例
 #### 活用例
-Bootstrap によるページの装飾方法の記述方法は [[こちらをご確認ください>./1. ページの装飾方法(Bootstrap)]]
+Bootstrap によるページの装飾方法の記述方法は [[こちらをご確認ください>./Bootstrap5]]
 
 
 
 
 # :memo:画像の挿入
 # :memo:画像の挿入
@@ -297,6 +297,6 @@ ___
 # :memo:さらに応用的な表現
 # :memo:さらに応用的な表現
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 
 
-- [図形の表現方法(Daigrams)](/Sandbox/Daigrams)
+- [図形の表現方法(Diagrams)](/Sandbox/Diagrams)
 
 
 - [数式の表現方法(Math)](/Sandbox/Math)
 - [数式の表現方法(Math)](/Sandbox/Math)

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

@@ -33,7 +33,7 @@ GROWI は法人・個人向けの wiki | ナレッジベースツールです。
 - アカウントを保有していない社外のユーザーのページ閲覧を可能にすることも可能です
 - アカウントを保有していない社外のユーザーのページ閲覧を可能にすることも可能です
     - 「共有リンク」を活用し社外のユーザーに情報を共有しましょう
     - 「共有リンク」を活用し社外のユーザーに情報を共有しましょう
 
 
-#### :bulb:ページの編集方法が分からないときは [Snadbox](/Sandbox) を確認してみましょう!
+#### :bulb:ページの編集方法が分からないときは [Sandbox](/Sandbox) を確認してみましょう!
 
 
 
 
 # :wrench:管理者の方へ ~ GROWI を作成したら~
 # :wrench:管理者の方へ ~ GROWI を作成したら~

+ 40 - 24
apps/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx → apps/app/src/components/Admin/SlackIntegration/BotTypeCard.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
+import Image from 'next/image';
 
 
 const botDetails = {
 const botDetails = {
   officialBot: {
   officialBot: {
@@ -30,25 +30,33 @@ const botDetails = {
   },
   },
 };
 };
 
 
-const BotTypeCard = (props) => {
+type BotTypeCardProps = {
+  isActive: boolean,
+  botType: string,
+  onBotTypeSelectHandler: (botType: SlackbotType) => void,
+};
+
+export const BotTypeCard = (props: BotTypeCardProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
+  const { isActive, botType, onBotTypeSelectHandler } = props;
+
+  const isBotTypeOfficial = botType === SlackbotType.OFFICIAL;
 
 
   return (
   return (
     <div
     <div
-      className={`card admin-bot-card rounded border-radius-sm shadow ${props.isActive ? 'border-primary' : ''}`}
-      onClick={() => props.onBotTypeSelectHandler(botDetails[props.botType].botType)}
+      className={`card admin-bot-card rounded border-radius-sm shadow ${isActive ? 'border-primary' : ''}`}
+      onClick={() => onBotTypeSelectHandler(botDetails[botType].botType)}
       role="button"
       role="button"
-      key={props.botType}
+      key={botType}
     >
     >
       <div>
       <div>
         <h3 className={`card-header mb-0 py-3
         <h3 className={`card-header mb-0 py-3
               ${isBotTypeOfficial ? 'd-flex align-items-center justify-content-center' : 'text-center'}
               ${isBotTypeOfficial ? 'd-flex align-items-center justify-content-center' : 'text-center'}
-              ${props.isActive ? 'bg-primary grw-botcard-title-active' : ''}`}
+              ${isActive ? 'bg-primary grw-botcard-title-active' : ''}`}
         >
         >
           <span className="me-2">
           <span className="me-2">
-            {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].botTypeCategory}`)}
+            {t(`admin:slack_integration.selecting_bot_types.${botDetails[botType].botTypeCategory}`)}
           </span>
           </span>
 
 
           {/*  A recommended badge is shown on official bot card, supplementary names are shown on Custom bot cards   */}
           {/*  A recommended badge is shown on official bot card, supplementary names are shown on Custom bot cards   */}
@@ -59,40 +67,48 @@ const BotTypeCard = (props) => {
               </span>
               </span>
             ) : (
             ) : (
               <span className="supplementary-bot-name me-2">
               <span className="supplementary-bot-name me-2">
-                {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].supplementaryBotName}`)}
+                {t(`admin:slack_integration.selecting_bot_types.${botDetails[botType].supplementaryBotName}`)}
               </span>
               </span>
             )}
             )}
 
 
-          <i className={props.isActive ? 'grw-botcard-title-active' : ''} aria-hidden="true"></i>
+          <i className={isActive ? 'grw-botcard-title-active' : ''} aria-hidden="true"></i>
         </h3>
         </h3>
       </div>
       </div>
       <div className="card-body p-4">
       <div className="card-body p-4">
         <div className="card-text">
         <div className="card-text">
           <div className="my-2">
           <div className="my-2">
-            <img
+            <Image
               className="bot-difficulty-icon d-block mx-auto mb-4"
               className="bot-difficulty-icon d-block mx-auto mb-4"
-              src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[props.botType].setUp}.svg`}
+              src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[botType].setUp}.svg`}
+              alt=""
+              width={60}
+              height={60}
             />
             />
-            <div className="d-flex justify-content-between mb-3">
+            <div className="d-flex justify-content-between mb-3 align-items-center">
               <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
               <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
-              <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].multiWSIntegration}.png`} alt="" />
+              <Image
+                className="bot-type-disc"
+                src={`/images/slack-integration/${botDetails[botType].multiWSIntegration}.png`}
+                alt=""
+                width={20}
+                height={20}
+              />
             </div>
             </div>
-            <div className="d-flex justify-content-between">
+            <div className="d-flex justify-content-between align-items-center">
               <span>{t('admin:slack_integration.selecting_bot_types.security_control')}</span>
               <span>{t('admin:slack_integration.selecting_bot_types.security_control')}</span>
-              <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].securityControl}.png`} alt="" />
+              <Image
+                className="bot-type-disc"
+                src={`/images/slack-integration/${botDetails[botType].securityControl}.png`}
+                alt=""
+                width={20}
+                height={20}
+              />
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
   );
   );
-
-};
-
-BotTypeCard.propTypes = {
-  isActive: PropTypes.bool.isRequired,
-  botType: PropTypes.string.isRequired,
-  onBotTypeSelectHandler: PropTypes.func.isRequired,
 };
 };
 
 
-export default BotTypeCard;
+BotTypeCard.displayName = 'BotTypeCard';

+ 19 - 23
apps/app/src/components/Admin/SlackIntegration/Bridge.jsx → apps/app/src/components/Admin/SlackIntegration/Bridge.tsx

@@ -1,19 +1,24 @@
-import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 const ProxyCircle = () => (
 const ProxyCircle = () => (
-  <div className="grw-bridge-proxy-circle">
-    <div className="circle position-absolute bg-primary border-light rounded-circle">
-      <p className="circle-inner text-light fw-bold d-none d-lg-inline">Proxy Server</p>
-      <p className="circle-inner grw-proxy-server-name d-block d-lg-none">Proxy Server</p>
+  <div className="grw-bridge-proxy-circle position-relative">
+    <div className="circle position-absolute m-auto z-1 bg-primary border-light rounded-circle">
+      <p className="circle-inner position-absolute text-light fw-bold d-none d-lg-inline">Proxy Server</p>
+      <p className="circle-inner position-absolute grw-proxy-server-name d-inline d-lg-none">Proxy Server</p>
     </div>
     </div>
   </div>
   </div>
 );
 );
 
 
-const BridgeCore = (props) => {
+type BridgeCoreProps = {
+  description: string,
+  iconClass: string,
+  iconName: string,
+  hrClass: string,
+  withProxy?: boolean,
+}
+const BridgeCore = (props: BridgeCoreProps): JSX.Element => {
   const {
   const {
     description, iconClass, iconName, hrClass, withProxy,
     description, iconClass, iconName, hrClass, withProxy,
   } = props;
   } = props;
@@ -44,16 +49,13 @@ const BridgeCore = (props) => {
   );
   );
 };
 };
 
 
-BridgeCore.propTypes = {
-  description: PropTypes.string.isRequired,
-  iconClass: PropTypes.string.isRequired,
-  iconName: PropTypes.string.isRequired,
-  hrClass: PropTypes.string.isRequired,
-  withProxy: PropTypes.bool,
-};
-
 
 
-const Bridge = (props) => {
+type BridgeProps = {
+  errorCount: number,
+  totalCount: number,
+  withProxy?: boolean,
+}
+export const Bridge = (props: BridgeProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { errorCount, totalCount, withProxy } = props;
   const { errorCount, totalCount, withProxy } = props;
 
 
@@ -95,10 +97,4 @@ const Bridge = (props) => {
   );
   );
 };
 };
 
 
-Bridge.propTypes = {
-  errorCount: PropTypes.number.isRequired,
-  totalCount: PropTypes.number.isRequired,
-  withProxy: PropTypes.bool,
-};
-
-export default Bridge;
+Bridge.displayName = 'Bridge';

+ 0 - 57
apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.jsx

@@ -1,57 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import Bridge from './Bridge';
-
-const CustomBotWithProxyConnectionStatus = (props) => {
-  const { siteName, connectionStatuses } = props;
-
-  const connectionStatusValues = Object.values(connectionStatuses); // type: ConnectionStatus[]
-
-  const totalCount = connectionStatusValues.length;
-  const errorCount = connectionStatusValues.filter(connectionStatus => connectionStatusValues.error != null).length;
-
-  return (
-    <div className="d-flex justify-content-center my-5 bot-integration">
-
-      <div className="card rounded shadow border-0 w-50 admin-bot-card">
-        <h5 className="card-title fw-bold mt-3 ms-3">Slack</h5>
-        <div className="card-body px-5">
-          {connectionStatusValues.map((connectionStatus, i) => {
-            const workspaceName = connectionStatus.workspaceName || `Settings #${i}`;
-
-            return (
-              <div key={workspaceName} className="card slack-work-space-name-card">
-                <div className="m-2 text-center">
-                  <h5 className="fw-bold">{workspaceName}</h5>
-                  <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
-                </div>
-              </div>
-            );
-          })}
-        </div>
-      </div>
-
-      <div className="text-center w-25 mt-3">
-        <Bridge errorCount={errorCount} totalCount={totalCount} withProxy />
-      </div>
-
-      <div className="card rounded-3 shadow border-0 w-50 admin-bot-card">
-        <h5 className="card-title fw-bold mt-3 ms-3">GROWI App</h5>
-        <div className="card-body text-center">
-          <div className="mx-md-3 my-4 my-lg-5 p-2 border bg-primary text-light">
-            {siteName}
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-CustomBotWithProxyConnectionStatus.propTypes = {
-  siteName: PropTypes.string.isRequired,
-  connectionStatuses: PropTypes.object.isRequired,
-};
-
-export default CustomBotWithProxyConnectionStatus;

+ 59 - 0
apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+
+import type { ConnectionStatus } from '@growi/slack';
+import Image from 'next/image';
+
+import { Bridge } from './Bridge';
+
+
+type CustomBotWithProxyConnectionStatusProps = {
+  siteName: string,
+  connectionStatuses: any,
+}
+
+export const CustomBotWithProxyConnectionStatus = (props: CustomBotWithProxyConnectionStatusProps): JSX.Element => {
+  const { siteName, connectionStatuses } = props;
+
+  const connectionStatusValues: ConnectionStatus[] = Object.values(connectionStatuses);
+
+  const totalCount = connectionStatusValues.length;
+  const errorCount = connectionStatusValues.filter(connectionStatus => connectionStatus.error != null).length;
+
+  return (
+    <div className="row justify-content-center my-5 bot-integration">
+
+      <div className="card rounded shadow col-4 border-0 admin-bot-card">
+        <h5 className="card-title fw-bold mt-3 text-center">Slack</h5>
+        <div className="card-body px-5">
+          {connectionStatusValues.map((connectionStatus, i) => {
+            const workspaceName = connectionStatus.workspaceName || `Settings #${i}`;
+
+            return (
+              <div key={workspaceName} className="card slack-work-space-name-card">
+                <div className="m-2 text-center">
+                  <h5 className="fw-bold">{workspaceName}</h5>
+                  <Image width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" alt="" />
+                </div>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+
+      <div className="col-3 mt-3 text-center">
+        <Bridge errorCount={errorCount} totalCount={totalCount} withProxy />
+      </div>
+
+      <div className="card rounded-3 shadow col-4 border-0 admin-bot-card">
+        <h5 className="card-title fw-bold mt-3 text-center">GROWI App</h5>
+        <div className="card-body text-center">
+          <div className="mx-md-3 my-4 my-lg-5 p-2 border bg-primary text-light">
+            {siteName}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+CustomBotWithProxyConnectionStatus.displayName = 'CustomBotWithProxyConnectionStatus';

+ 2 - 2
apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -9,8 +9,8 @@ import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
-import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
-import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { CustomBotWithProxyConnectionStatus } from './CustomBotWithProxyConnectionStatus';
+import { DeleteSlackBotSettingsModal } from './DeleteSlackBotSettingsModal';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import WithProxyAccordions from './WithProxyAccordions';
 import WithProxyAccordions from './WithProxyAccordions';
 
 

+ 0 - 57
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.jsx

@@ -1,57 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import Bridge from './Bridge';
-
-const CustomBotWithoutProxyConnectionStatus = (props) => {
-  const { siteName, connectionStatuses } = props;
-
-  const connectionStatusValues = Object.values(connectionStatuses); // type: ConnectionStatus[]
-
-  const totalCount = connectionStatusValues.length;
-  const errorCount = connectionStatusValues.filter(connectionStatus => connectionStatus.error != null).length;
-
-  let workspaceName;
-  if (totalCount > 0) {
-    workspaceName = connectionStatusValues[0].workspaceName;
-  }
-
-  return (
-    <div className="d-flex justify-content-center my-5 bot-integration">
-      <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
-        <h5 className="card-title fw-bold mt-3 ms-4">Slack</h5>
-        <div className="card-body px-4 text-center mx-md-5">
-          {totalCount > 0 ? (
-            <div className="card slack-work-space-name-card">
-              <div className="m-2 text-center">
-                <h5 className="fw-bold">
-                  {workspaceName != null ? workspaceName : 'Settings #1'}
-                </h5>
-                <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
-              </div>
-            </div>
-          ) : ''}
-        </div>
-      </div>
-
-      <div className="text-center w-25">
-        <Bridge errorCount={errorCount} totalCount={totalCount} />
-      </div>
-
-      <div className="card rounded-3 shadow border-0 w-50 admin-bot-card mb-0">
-        <h5 className="card-title fw-bold mt-3 ms-4">GROWI App</h5>
-        <div className="card-body p-4 text-center">
-          <div className="border p-2 bg-primary text-light mx-md-5">
-            {siteName}
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-CustomBotWithoutProxyConnectionStatus.propTypes = {
-  siteName: PropTypes.string.isRequired,
-  connectionStatuses: PropTypes.object.isRequired,
-};
-
-export default CustomBotWithoutProxyConnectionStatus;

+ 57 - 0
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+
+import type { ConnectionStatus } from '@growi/slack';
+import Image from 'next/image';
+
+import { Bridge } from './Bridge';
+
+type CustomBotWithoutProxyConnectionStatusProps = {
+  siteName: string,
+  connectionStatuses: any,
+}
+
+export const CustomBotWithoutProxyConnectionStatus = (props: CustomBotWithoutProxyConnectionStatusProps): JSX.Element => {
+  const { siteName, connectionStatuses } = props;
+
+  const connectionStatusValues: ConnectionStatus[] = Object.values(connectionStatuses);
+
+  const totalCount = connectionStatusValues.length;
+  const errorCount = connectionStatusValues.filter(connectionStatus => connectionStatus.error != null).length;
+  const workspaceName = connectionStatusValues[0]?.workspaceName;
+
+  return (
+    <div className="row justify-content-center my-5 bot-integration">
+      <div className="card rounded shadow col-4 border-0 admin-bot-card mb-0">
+        <h5 className="card-title fw-bold mt-3 text-center">Slack</h5>
+        <div className="card-body px-4 text-center mx-md-5">
+          {totalCount > 0 ? (
+            <div className="card slack-work-space-name-card">
+              <div className="m-2 text-center">
+                <h5 className="fw-bold">
+                  {workspaceName != null ? workspaceName : 'Settings #1'}
+                </h5>
+                <Image width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" alt="" />
+              </div>
+            </div>
+          ) : ''}
+        </div>
+      </div>
+
+      <div className="col-3 text-center">
+        <Bridge errorCount={errorCount} totalCount={totalCount} />
+      </div>
+
+      <div className="card rounded-3 shadow col-4 border-0 admin-bot-card mb-0">
+        <h5 className="card-title fw-bold mt-3 text-center">GROWI App</h5>
+        <div className="card-body p-4 text-center">
+          <div className="border p-2 bg-primary text-light mx-md-5">
+            {siteName}
+          </div>
+        </div>
+      </div>
+
+    </div>
+  );
+};
+
+CustomBotWithoutProxyConnectionStatus.displayName = 'CustomBotWithoutProxyConnectionStatus';

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 
 import { useAppTitle } from '~/stores/context';
 import { useAppTitle } from '~/stores/context';
 
 
-import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
+import { CustomBotWithoutProxyConnectionStatus } from './CustomBotWithoutProxyConnectionStatus';
 import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 
 const CustomBotWithoutProxySettings = (props) => {
 const CustomBotWithoutProxySettings = (props) => {

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -10,7 +10,7 @@ import Accordion from '../Common/Accordion';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
 import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
-import { addLogs } from './slak-integration-util';
+import { addLogs } from './slack-integration-util';
 
 
 
 
 export const botInstallationStep = {
 export const botInstallationStep = {

+ 21 - 19
apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx → apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx

@@ -1,13 +1,24 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import {
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-const DeleteSlackBotSettingsModal = React.memo((props) => {
-  const { t, onClickDeleteButton, onClose } = useTranslation();
+type DeleteSlackBotSettingsModalProps = {
+  isResetAll: boolean,
+  isOpen: boolean,
+  onClose?: () => void,
+  onClickDeleteButton?: () => void,
+}
+
+export const DeleteSlackBotSettingsModal = React.memo((props: DeleteSlackBotSettingsModalProps) => {
+
+  const { t } = useTranslation();
+
+  const {
+    isResetAll, isOpen, onClose, onClickDeleteButton,
+  } = props;
 
 
   const deleteSlackCredentialsHandler = useCallback(() => {
   const deleteSlackCredentialsHandler = useCallback(() => {
     onClickDeleteButton?.();
     onClickDeleteButton?.();
@@ -19,16 +30,16 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
   }, [onClose]);
   }, [onClose]);
 
 
   return (
   return (
-    <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
+    <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
         <span>
         <span>
-          {props.isResetAll && (
+          {isResetAll && (
             <>
             <>
               <span className="material-symbols-outlined">delete_forever</span>
               <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset_all_settings')}
               {t('admin:slack_integration.reset_all_settings')}
             </>
             </>
           )}
           )}
-          {!props.isResetAll && (
+          {!isResetAll && (
             <>
             <>
               <span className="material-symbols-outlined">delete</span>
               <span className="material-symbols-outlined">delete</span>
               {t('admin:slack_integration.delete_slackbot_settings')}
               {t('admin:slack_integration.delete_slackbot_settings')}
@@ -37,13 +48,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         </span>
         </span>
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        {props.isResetAll && (
+        {isResetAll && (
           <span
           <span
             // eslint-disable-next-line react/no-danger
             // eslint-disable-next-line react/no-danger
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
           />
           />
         )}
         )}
-        {!props.isResetAll && (
+        {!isResetAll && (
           <span
           <span
             // eslint-disable-next-line react/no-danger
             // eslint-disable-next-line react/no-danger
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.slackbot_settings_notice') }}
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.slackbot_settings_notice') }}
@@ -53,13 +64,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
       <ModalFooter>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
-          {props.isResetAll && (
+          {isResetAll && (
             <>
             <>
               <span className="material-symbols-outlined">delete_forever</span>
               <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset')}
               {t('admin:slack_integration.reset')}
             </>
             </>
           )}
           )}
-          {!props.isResetAll && (
+          {!isResetAll && (
             <>
             <>
               <span className="material-symbols-outlined">delete</span>
               <span className="material-symbols-outlined">delete</span>
               {t('admin:slack_integration.delete')}
               {t('admin:slack_integration.delete')}
@@ -72,13 +83,4 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
 
 
 });
 });
 
 
-DeleteSlackBotSettingsModal.propTypes = {
-  isResetAll: PropTypes.bool.isRequired,
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func,
-  onClickDeleteButton: PropTypes.func,
-};
-
 DeleteSlackBotSettingsModal.displayName = 'DeleteSlackBotSettingsModal';
 DeleteSlackBotSettingsModal.displayName = 'DeleteSlackBotSettingsModal';
-
-export default DeleteSlackBotSettingsModal;

+ 2 - 2
apps/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -11,8 +11,8 @@ import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
-import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
-import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { CustomBotWithProxyConnectionStatus } from './CustomBotWithProxyConnectionStatus';
+import { DeleteSlackBotSettingsModal } from './DeleteSlackBotSettingsModal';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import WithProxyAccordions from './WithProxyAccordions';
 import WithProxyAccordions from './WithProxyAccordions';
 
 

+ 1 - 2
apps/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx

@@ -1,4 +1,3 @@
-import React, { FC } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -11,7 +10,7 @@ type Props = {
   onDeleteButtonClicked?: (slackAppIntegration: unknown) => void,
   onDeleteButtonClicked?: (slackAppIntegration: unknown) => void,
 }
 }
 
 
-export const SlackAppIntegrationControl: FC<Props> = (props: Props) => {
+export const SlackAppIntegrationControl = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const { slackAppIntegration, onIsPrimaryChanged, onDeleteButtonClicked } = props;
   const { slackAppIntegration, onIsPrimaryChanged, onDeleteButtonClicked } = props;

+ 12 - 12
apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx → apps/app/src/components/Admin/SlackIntegration/SlackIntegration.tsx

@@ -10,21 +10,21 @@ import {
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
 
-import BotTypeCard from './BotTypeCard';
+import { BotTypeCard } from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
-import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { DeleteSlackBotSettingsModal } from './DeleteSlackBotSettingsModal';
 import OfficialBotSettings from './OfficialBotSettings';
 import OfficialBotSettings from './OfficialBotSettings';
 
 
 
 
 const botTypes = Object.values(SlackbotType);
 const botTypes = Object.values(SlackbotType);
 
 
-const SlackIntegration = () => {
+export const SlackIntegration = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const [currentBotType, setCurrentBotType] = useState(null);
-  const [selectedBotType, setSelectedBotType] = useState(null);
+  const [currentBotType, setCurrentBotType] = useState<SlackbotType | undefined>();
+  const [selectedBotType, setSelectedBotType] = useState<SlackbotType | undefined>();
   const [slackSigningSecret, setSlackSigningSecret] = useState(null);
   const [slackSigningSecret, setSlackSigningSecret] = useState(null);
   const [slackBotToken, setSlackBotToken] = useState(null);
   const [slackBotToken, setSlackBotToken] = useState(null);
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
@@ -106,12 +106,12 @@ const SlackIntegration = () => {
     fetchSlackIntegrationData();
     fetchSlackIntegrationData();
   }, [fetchSlackIntegrationData]);
   }, [fetchSlackIntegrationData]);
 
 
-  const changeCurrentBotSettings = async(botType) => {
+  const changeCurrentBotSettings = async(botType?: SlackbotType) => {
     try {
     try {
       await apiv3Put('/slack-integration-settings/bot-type', {
       await apiv3Put('/slack-integration-settings/bot-type', {
         currentBotType: botType,
         currentBotType: botType,
       });
       });
-      setSelectedBotType(null);
+      setSelectedBotType(undefined);
       fetchSlackIntegrationData();
       fetchSlackIntegrationData();
     }
     }
     catch (err) {
     catch (err) {
@@ -119,7 +119,7 @@ const SlackIntegration = () => {
     }
     }
   };
   };
 
 
-  const botTypeSelectHandler = async(botType) => {
+  const botTypeSelectHandler = async(botType: SlackbotType) => {
     if (botType === currentBotType) {
     if (botType === currentBotType) {
       return;
       return;
     }
     }
@@ -135,10 +135,10 @@ const SlackIntegration = () => {
   };
   };
 
 
   const cancelBotChangeHandler = () => {
   const cancelBotChangeHandler = () => {
-    setSelectedBotType(null);
+    setSelectedBotType(undefined);
   };
   };
 
 
-  let settingsComponent = null;
+  let settingsComponent = <></>;
 
 
   switch (currentBotType) {
   switch (currentBotType) {
     case SlackbotType.OFFICIAL:
     case SlackbotType.OFFICIAL:
@@ -231,7 +231,7 @@ const SlackIntegration = () => {
           </button>
           </button>
         </div>
         </div>
 
 
-        <div className="row my-5 flex-wrap-reverse justify-content-center">
+        <div className="my-5 d-flex flex-wrap-reverse justify-content-center">
           {botTypes.map((botType) => {
           {botTypes.map((botType) => {
             return (
             return (
               <div key={botType} className="m-3">
               <div key={botType} className="m-3">
@@ -251,4 +251,4 @@ const SlackIntegration = () => {
   );
   );
 };
 };
 
 
-export default SlackIntegration;
+SlackIntegration.displayName = 'SlackIntegration';

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -15,7 +15,7 @@ import Accordion from '../Common/Accordion';
 
 
 import ManageCommandsProcess from './ManageCommandsProcess';
 import ManageCommandsProcess from './ManageCommandsProcess';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
-import { addLogs } from './slak-integration-util';
+import { addLogs } from './slack-integration-util';
 
 
 const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 
 

+ 4 - 0
apps/app/src/components/Admin/SlackIntegration/slack-integration-util.ts

@@ -0,0 +1,4 @@
+export const addLogs = (log: string, newLogMessage:string, newLogCode?: string): string => {
+  const newLog = `${new Date()} - ${newLogCode ? `${newLogCode}, ` : ''}${newLogMessage}\n\n`;
+  return `${newLog}${log ?? ''}`;
+};

+ 0 - 20
apps/app/src/components/Admin/SlackIntegration/slak-integration-util.js

@@ -1,20 +0,0 @@
-const addLogs = (log, newLogMessage, newLogCode = undefined) => {
-
-  let newLog;
-  if (newLogCode == null) {
-    newLog = `${new Date()} - ${newLogMessage}\n\n`;
-  }
-  else {
-    newLog = `${new Date()} - ${newLogCode}, ${newLogMessage}\n\n`;
-  }
-
-  if (log == null) {
-    return newLog;
-  }
-  return `${newLog}${log}`;
-};
-
-export {
-  // eslint-disable-next-line import/prefer-default-export
-  addLogs,
-};

+ 8 - 1
apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -162,7 +162,14 @@ export const UserGroupTable: FC<Props> = ({
                 {isExternalGroup && <td>{(group as IExternalUserGroupHasId).provider}</td>}
                 {isExternalGroup && <td>{(group as IExternalUserGroupHasId).provider}</td>}
                 {isAclEnabled
                 {isAclEnabled
                   ? (
                   ? (
-                    <td><Link href={`/admin/user-group-detail/${group._id}?isExternalGroup=${isExternalGroup}`}>{group.name}</Link></td>
+                    <td>
+                      <Link
+                        className="link-opacity-75-hover"
+                        href={`/admin/user-group-detail/${group._id}?isExternalGroup=${isExternalGroup}`}
+                      >
+                        {group.name}
+                      </Link>
+                    </td>
                   )
                   )
                   : (
                   : (
                     <td>{group.name}</td>
                     <td>{group.name}</td>

+ 9 - 8
apps/app/src/components/Layout/Admin.module.scss

@@ -95,9 +95,12 @@ $slack-work-space-name-card-border: #efc1f6;
       font-size: 0.6rem;
       font-size: 0.6rem;
     }
     }
     .admin-bot-card {
     .admin-bot-card {
-      min-width: 280px;
+      min-width: 300px;
       max-width: 500px;
       max-width: 500px;
       border-radius: 8px !important;
       border-radius: 8px !important;
+      .grw-botcard-title-active {
+        color: $gray-200;
+      }
     }
     }
     .border-primary {
     .border-primary {
       border-width: 2px;
       border-width: 2px;
@@ -135,12 +138,12 @@ $slack-work-space-name-card-border: #efc1f6;
 
 
     .grw-bridge-proxy-circle {
     .grw-bridge-proxy-circle {
       .circle {
       .circle {
-        left: 50%;
+        inset: 0;
         width: 100px;
         width: 100px;
         height: 100px;
         height: 100px;
         border: 13px solid;
         border: 13px solid;
-        transform: translate(-50%, -50%);
-        @include media-breakpoint-down(md) {
+
+        @include media-breakpoint-down(lg) {
           width: 50px;
           width: 50px;
           height: 50px;
           height: 50px;
           border: 8px solid;
           border: 8px solid;
@@ -148,13 +151,11 @@ $slack-work-space-name-card-border: #efc1f6;
       }
       }
 
 
       .circle-inner {
       .circle-inner {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
+        transform: translate(-50%, 25%);
       }
       }
       .circle-inner.grw-proxy-server-name {
       .circle-inner.grw-proxy-server-name {
         margin-top: 55px;
         margin-top: 55px;
+        transform: translate(-50%, -25%);
       }
       }
     }
     }
 
 

+ 8 - 7
apps/app/src/components/PageControls/PageControls.tsx

@@ -235,7 +235,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     return wideviewMenuItemRenderer;
     return wideviewMenuItemRenderer;
   }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
   }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
 
 
-  if (!isIPageInfoForOperation(pageInfo)) {
+  if (!isIPageInfoForEntity(pageInfo)) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -249,6 +249,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     MenuItemType.REVERT,
     MenuItemType.REVERT,
   ];
   ];
 
 
+  const _isIPageInfoForOperation = isIPageInfoForOperation(pageInfo);
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return (
   return (
@@ -256,18 +257,18 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
       { isDeviceLargerThanMd && (
       { isDeviceLargerThanMd && (
         <SearchButton />
         <SearchButton />
       )}
       )}
-      {revisionId != null && !isViewMode && (
+      {revisionId != null && !isViewMode && _isIPageInfoForOperation && (
         <Tags
         <Tags
           onClickEditTagsButton={onClickEditTagsButton}
           onClickEditTagsButton={onClickEditTagsButton}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <SubscribeButton
         <SubscribeButton
           status={pageInfo.subscriptionStatus}
           status={pageInfo.subscriptionStatus}
           onClick={subscribeClickhandler}
           onClick={subscribeClickhandler}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <LikeButtons
         <LikeButtons
           onLikeClicked={likeClickhandler}
           onLikeClicked={likeClickhandler}
           sumOfLikers={sumOfLikers}
           sumOfLikers={sumOfLikers}
@@ -275,7 +276,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
           likers={likers}
           likers={likers}
         />
         />
       )}
       )}
-      {revisionId != null && (
+      {revisionId != null && _isIPageInfoForOperation && (
         <BookmarkButtons
         <BookmarkButtons
           pageId={pageId}
           pageId={pageId}
           isBookmarked={pageInfo.isBookmarked}
           isBookmarked={pageInfo.isBookmarked}
@@ -289,7 +290,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
           disabled={disableSeenUserInfoPopover}
           disabled={disableSeenUserInfoPopover}
         />
         />
       ) }
       ) }
-      { showPageControlDropdown && (
+      { showPageControlDropdown && _isIPageInfoForOperation && (
         <PageItemControl
         <PageItemControl
           alignEnd
           alignEnd
           pageId={pageId}
           pageId={pageId}
@@ -337,7 +338,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
-  if (!isIPageInfoForOperation(pageInfo)) {
+  if (!isIPageInfoForEntity(pageInfo)) {
     return <></>;
     return <></>;
   }
   }
 
 

+ 1 - 0
apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss

@@ -5,6 +5,7 @@
 @include mixins.editing() {
 @include mixins.editing() {
   .grw-editor-navbar-bottom :global {
   .grw-editor-navbar-bottom :global {
     .grw-grant-selector {
     .grw-grant-selector {
+      max-width: 250px;
       .material-symbols-outlined  {
       .material-symbols-outlined  {
         padding-bottom: 2px;
         padding-bottom: 2px;
         font-size: 19px;
         font-size: 19px;

+ 6 - 2
apps/app/src/components/PageEditor/PageEditorReadOnly.tsx

@@ -11,7 +11,11 @@ import { EditorNavbar } from './EditorNavbar';
 import Preview from './Preview';
 import Preview from './Preview';
 import { useScrollSync } from './ScrollSyncHelper';
 import { useScrollSync } from './ScrollSyncHelper';
 
 
-export const PageEditorReadOnly = react.memo((): JSX.Element => {
+type Props = {
+  visibility?: boolean,
+}
+
+export const PageEditorReadOnly = react.memo(({ visibility }: Props): JSX.Element => {
   const previewRef = useRef<HTMLDivElement>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
@@ -30,7 +34,7 @@ export const PageEditorReadOnly = react.memo((): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div id="page-editor" className="flex-expand-vert">
+    <div id="page-editor" className={`flex-expand-vert ${visibility ? '' : 'd-none'}`}>
       <EditorNavbar />
       <EditorNavbar />
 
 
       <div className="flex-expand-horiz">
       <div className="flex-expand-horiz">

+ 26 - 8
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -144,11 +144,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
           <span className="label">
           <span className="label">
             {userRelatedGrantedGroups.length > 1
             {userRelatedGrantedGroups.length > 1
               ? (
               ? (
+              // substring for group name truncate
                 <span>
                 <span>
-                  {`${userRelatedGrantedGroups[0].name}... `}
-                  <span className="badge badge-purple">+{userRelatedGrantedGroups.length - 1}</span>
+                  {`${userRelatedGrantedGroups[0].name.substring(0, 30)}, ... `}
+                  <span className="badge bg-primary">+{userRelatedGrantedGroups.length - 1}</span>
                 </span>
                 </span>
-              ) : userRelatedGrantedGroups[0].name}
+              ) : userRelatedGrantedGroups[0].name.substring(0, 30)}
           </span>
           </span>
         </span>
         </span>
       );
       );
@@ -162,7 +163,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
     return (
     return (
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
         <UncontrolledDropdown direction={openInModal ? 'down' : 'up'} size="sm">
         <UncontrolledDropdown direction={openInModal ? 'down' : 'up'} size="sm">
-          <DropdownToggle color={dropdownToggleBtnColor} caret className="w-100 d-flex justify-content-between align-items-center" disabled={disabled}>
+          <DropdownToggle
+            color={dropdownToggleBtnColor}
+            caret
+            className="w-100 text-truncate d-flex justify-content-between align-items-center"
+            disabled={disabled}
+          >
             {dropdownToggleLabelElm}
             {dropdownToggleLabelElm}
           </DropdownToggle>
           </DropdownToggle>
           <DropdownMenu container={openInModal ? '' : 'body'}>
           <DropdownMenu container={openInModal ? '' : 'body'}>
@@ -209,13 +215,13 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
           return (
           return (
             <button
             <button
-              className={`btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 ${activeClass}`}
+              className={`btn btn-outline-primary d-flex justify-content-start mb-3 mx-4 align-items-center p-3 ${activeClass}`}
               type="button"
               type="button"
               key={group.item._id}
               key={group.item._id}
               onClick={() => groupListItemClickHandler(group)}
               onClick={() => groupListItemClickHandler(group)}
             >
             >
-              <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
-              <h5 className="d-inline-block ms-3">{group.item.name}</h5>
+              <input type="checkbox" checked={groupIsGranted} />
+              <p className="ms-3 mb-0">{group.item.name}</p>
               {group.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{group.item.provider}</span>}
               {group.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{group.item.provider}</span>}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
             </button>
@@ -227,6 +233,18 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
 
 
+  const renderModalCloseButton = useCallback(() => {
+    return (
+      <button
+        type="button"
+        className="btn border-0 text-muted"
+        onClick={() => setIsSelectGroupModalShown(false)}
+      >
+        <span className="material-symbols-outlined">close</span>
+      </button>
+    );
+  }, [setIsSelectGroupModalShown]);
+
   return (
   return (
     <>
     <>
       { renderGrantSelector() }
       { renderGrantSelector() }
@@ -238,7 +256,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
           toggle={() => setIsSelectGroupModalShown(false)}
           toggle={() => setIsSelectGroupModalShown(false)}
           centered
           centered
         >
         >
-          <ModalHeader tag="h4" toggle={() => setIsSelectGroupModalShown(false)} className="bg-purple text-muted">
+          <ModalHeader tag="p" toggle={() => setIsSelectGroupModalShown(false)} className="fs-5 text-muted fw-bold pb-2" close={renderModalCloseButton()}>
             {t('user_group.select_group')}
             {t('user_group.select_group')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>

+ 5 - 4
apps/app/src/pages/admin/slack-integration.page.tsx

@@ -1,19 +1,20 @@
-import {
+import type {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CommonProps } from '~/pages/utils/commons';
+import { generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser, useSiteUrl } from '~/stores/context';
 import { useCurrentUser, useSiteUrl } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 
 
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const SlackIntegration = dynamic(() => import('~/components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
+const SlackIntegration = dynamic(() => import('~/components/Admin/SlackIntegration/SlackIntegration').then(mod => mod.SlackIntegration), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 
 

+ 7 - 0
apps/app/src/server/crowi/index.js

@@ -37,8 +37,10 @@ import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
 import { SlackIntegrationService } from '../service/slack-integration';
 import UserGroupService from '../service/user-group';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
 import { UserNotificationService } from '../service/user-notification';
+import { instantiateYjsConnectionManager } from '../service/yjs-connection-manager';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
 
+
 const logger = loggerFactory('growi:crowi');
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 
 
@@ -475,9 +477,14 @@ Crowi.prototype.start = async function() {
 
 
   // setup terminus
   // setup terminus
   this.setupTerminus(httpServer);
   this.setupTerminus(httpServer);
+
   // attach to socket.io
   // attach to socket.io
   this.socketIoService.attachServer(httpServer);
   this.socketIoService.attachServer(httpServer);
 
 
+  // Initialization YjsConnectionManager
+  instantiateYjsConnectionManager(this.socketIoService.io);
+  this.socketIoService.setupYjsConnection();
+
   // listen
   // listen
   const serverListening = httpServer.listen(this.port, () => {
   const serverListening = httpServer.listen(this.port, () => {
     logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
     logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);

+ 9 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -1,4 +1,4 @@
-import { allOrigin } from '@growi/core';
+import { Origin, allOrigin } from '@growi/core';
 import type {
 import type {
   IPage, IRevisionHasId, IUserHasId,
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
 } from '@growi/core';
@@ -20,6 +20,7 @@ import {
 import type { PageDocument, PageModel } from '~/server/models/page';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { preNotifyService } from '~/server/service/pre-notify';
+import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
 import XssOption from '~/services/xss/xssOption';
 import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -79,6 +80,13 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
 
 
 
   async function postAction(req: UpdatePageRequest, res: ApiV3Response, updatedPage: PageDocument) {
   async function postAction(req: UpdatePageRequest, res: ApiV3Response, updatedPage: PageDocument) {
+    // Reflect the updates in ydoc
+    const origin = req.body.origin;
+    if (origin === Origin.View || origin === undefined) {
+      const yjsConnectionManager = getYjsConnectionManager();
+      await yjsConnectionManager.handleYDocUpdate(req.body.pageId, req.body.body);
+    }
+
     // persist activity
     // persist activity
     const parameters = {
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,
       targetModel: SupportedTargetModel.MODEL_PAGE,

+ 3 - 6
apps/app/src/server/service/socket-io.js

@@ -5,7 +5,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 
-import YjsConnectionManager from './yjs-connection-manager';
+import { getYjsConnectionManager } from './yjs-connection-manager';
 
 
 const expressSession = require('express-session');
 const expressSession = require('express-session');
 const passport = require('passport');
 const passport = require('passport');
@@ -37,9 +37,6 @@ class SocketIoService {
     });
     });
     this.io.attach(server);
     this.io.attach(server);
 
 
-    // create the YjsConnectionManager instance
-    this.yjsConnectionManager = new YjsConnectionManager(this.io);
-
     // create namespace for admin
     // create namespace for admin
     this.adminNamespace = this.io.of('/admin');
     this.adminNamespace = this.io.of('/admin');
 
 
@@ -54,7 +51,6 @@ class SocketIoService {
 
 
     await this.setupLoginedUserRoomsJoinOnConnection();
     await this.setupLoginedUserRoomsJoinOnConnection();
     await this.setupDefaultSocketJoinRoomsEventHandler();
     await this.setupDefaultSocketJoinRoomsEventHandler();
-    await this.setupYjsConnection();
   }
   }
 
 
   getDefaultSocket() {
   getDefaultSocket() {
@@ -160,10 +156,11 @@ class SocketIoService {
   }
   }
 
 
   setupYjsConnection() {
   setupYjsConnection() {
+    const yjsConnectionManager = getYjsConnectionManager();
     this.io.on('connection', (socket) => {
     this.io.on('connection', (socket) => {
       socket.on(GlobalSocketEventName.YDocSync, async({ pageId, initialValue }) => {
       socket.on(GlobalSocketEventName.YDocSync, async({ pageId, initialValue }) => {
         try {
         try {
-          await this.yjsConnectionManager.handleYDocSync(pageId, initialValue);
+          await yjsConnectionManager.handleYDocSync(pageId, initialValue);
         }
         }
         catch (error) {
         catch (error) {
           logger.warn(error.message);
           logger.warn(error.message);

+ 35 - 5
apps/app/src/server/service/yjs-connection-manager.ts

@@ -5,16 +5,18 @@ import * as Y from 'yjs';
 
 
 import { getMongoUri } from '../util/mongoose-utils';
 import { getMongoUri } from '../util/mongoose-utils';
 
 
-export const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
-export const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
+const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
+const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
 
 
 class YjsConnectionManager {
 class YjsConnectionManager {
 
 
+  private static instance: YjsConnectionManager;
+
   private ysocketio: YSocketIO;
   private ysocketio: YSocketIO;
 
 
   private mdb: MongodbPersistence;
   private mdb: MongodbPersistence;
 
 
-  constructor(io: Server) {
+  private constructor(io: Server) {
     this.ysocketio = new YSocketIO(io);
     this.ysocketio = new YSocketIO(io);
     this.ysocketio.initialize();
     this.ysocketio.initialize();
 
 
@@ -22,8 +24,19 @@ class YjsConnectionManager {
       collectionName: MONGODB_PERSISTENCE_COLLECTION_NAME,
       collectionName: MONGODB_PERSISTENCE_COLLECTION_NAME,
       flushSize: MONGODB_PERSISTENCE_FLUSH_SIZE,
       flushSize: MONGODB_PERSISTENCE_FLUSH_SIZE,
     });
     });
+  }
+
+  public static getInstance(io?: Server) {
+    if (this.instance != null) {
+      return this.instance;
+    }
+
+    if (io == null) {
+      throw new Error("'io' is required if initialize YjsConnectionManager");
+    }
 
 
-    this.getCurrentYdoc = this.getCurrentYdoc.bind(this);
+    this.instance = new YjsConnectionManager(io);
+    return this.instance;
   }
   }
 
 
   public async handleYDocSync(pageId: string, initialValue: string): Promise<void> {
   public async handleYDocSync(pageId: string, initialValue: string): Promise<void> {
@@ -60,6 +73,16 @@ class YjsConnectionManager {
     persistedYdoc.destroy();
     persistedYdoc.destroy();
   }
   }
 
 
+  public async handleYDocUpdate(pageId: string, newValue: string): Promise<void> {
+    // TODO: https://redmine.weseek.co.jp/issues/132775
+    // It's necessary to confirm that the user is not editing the target page in the Editor
+    const currentYdoc = this.getCurrentYdoc(pageId);
+    const currentMarkdownLength = currentYdoc.getText('codemirror').length;
+    currentYdoc.getText('codemirror').delete(0, currentMarkdownLength);
+    currentYdoc.getText('codemirror').insert(0, newValue);
+    Y.encodeStateAsUpdate(currentYdoc);
+  }
+
   private getCurrentYdoc(pageId: string): Y.Doc {
   private getCurrentYdoc(pageId: string): Y.Doc {
     const currentYdoc = this.ysocketio.documents.get(`yjs/${pageId}`);
     const currentYdoc = this.ysocketio.documents.get(`yjs/${pageId}`);
     if (currentYdoc == null) {
     if (currentYdoc == null) {
@@ -70,4 +93,11 @@ class YjsConnectionManager {
 
 
 }
 }
 
 
-export default YjsConnectionManager;
+export const instantiateYjsConnectionManager = (io: Server): YjsConnectionManager => {
+  return YjsConnectionManager.getInstance(io);
+};
+
+// export the singleton instance
+export const getYjsConnectionManager = (): YjsConnectionManager => {
+  return YjsConnectionManager.getInstance();
+};

+ 17 - 15
packages/editor/src/services/paste-util/paste-markdown-util.ts

@@ -20,28 +20,30 @@ export const adjustPasteData = (strFromBol: string, text: string): string => {
 
 
   let adjusted = text;
   let adjusted = text;
 
 
-  if (text.match(indentAndMarkRE)) {
-    const matchResult = strFromBol.match(indentAndMarkRE);
-    const indent = matchResult ? matchResult[1] : '';
+  if (indentAndMarkOnlyRE.test(strFromBol)) {
+    if (text.match(indentAndMarkRE)) {
+      const matchResult = strFromBol.match(indentAndMarkRE);
+      const indent = matchResult ? matchResult[1] : '';
 
 
-    const lines = text.match(/[^\r\n]+/g);
+      const lines = text.match(/[^\r\n]+/g);
 
 
-    const replacedLines = lines?.map((line, index) => {
+      const replacedLines = lines?.map((line, index) => {
 
 
-      if (index === 0 && strFromBol.match(indentAndMarkOnlyRE)) {
-        return line.replace(indentAndMarkRE, '');
-      }
+        if (index === 0 && strFromBol.match(indentAndMarkOnlyRE)) {
+          return line.replace(indentAndMarkRE, '');
+        }
 
 
-      return indent + line;
-    });
+        return indent + line;
+      });
 
 
-    adjusted = replacedLines ? replacedLines.join('\n') : '';
-  }
+      adjusted = replacedLines ? replacedLines.join('\n') : '';
+    }
 
 
-  else if (strFromBol.match(indentAndMarkRE)) {
-    const replacedText = text.replace(/(\r\n|\r|\n)/g, `$1${strFromBol}`);
+    else {
+      const replacedText = text.replace(/(\r\n|\r|\n)/g, `$1${strFromBol}`);
 
 
-    adjusted = replacedText;
+      adjusted = replacedText;
+    }
   }
   }
 
 
   return adjusted;
   return adjusted;

+ 1 - 0
packages/slack/src/interfaces/index.ts

@@ -1,4 +1,5 @@
 export * from './channel';
 export * from './channel';
+export * from './connection-status';
 export * from './growi-command-processor';
 export * from './growi-command-processor';
 export * from './growi-interaction-processor';
 export * from './growi-interaction-processor';
 export * from './growi-event-processor';
 export * from './growi-event-processor';