Răsfoiți Sursa

Merge branch 'feat/page-bulk-export' into feat/78038-135783-multipart-upload-page-export

Futa Arai 2 ani în urmă
părinte
comite
91451da408
50 a modificat fișierele cu 435 adăugiri și 323 ștergeri
  1. 0 6
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  2. 0 1
      apps/app/next-env.d.ts
  3. 1 0
      apps/app/public/static/locales/en_US/translation.json
  4. 1 0
      apps/app/public/static/locales/ja_JP/translation.json
  5. 1 0
      apps/app/public/static/locales/zh_CN/translation.json
  6. 1 1
      apps/app/resource/locales/en_US/welcome.md
  7. 1 1
      apps/app/resource/locales/ja_JP/welcome.md
  8. 2 2
      apps/app/src/components/Admin/App/AppSetting.jsx
  9. 40 24
      apps/app/src/components/Admin/SlackIntegration/BotTypeCard.tsx
  10. 19 23
      apps/app/src/components/Admin/SlackIntegration/Bridge.tsx
  11. 0 57
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.jsx
  12. 59 0
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.tsx
  13. 3 3
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  14. 0 57
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.jsx
  15. 57 0
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.tsx
  16. 2 2
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  17. 3 3
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  18. 21 19
      apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx
  19. 3 3
      apps/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  20. 1 2
      apps/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx
  21. 12 12
      apps/app/src/components/Admin/SlackIntegration/SlackIntegration.tsx
  22. 5 5
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  23. 4 0
      apps/app/src/components/Admin/SlackIntegration/slack-integration-util.ts
  24. 0 20
      apps/app/src/components/Admin/SlackIntegration/slak-integration-util.js
  25. 8 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  26. 6 2
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  27. 9 8
      apps/app/src/components/Layout/Admin.module.scss
  28. 2 2
      apps/app/src/components/Me/UISettings.tsx
  29. 1 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  30. 8 7
      apps/app/src/components/PageControls/PageControls.tsx
  31. 1 1
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  32. 1 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss
  33. 1 1
      apps/app/src/components/PageHeader/PageHeader.tsx
  34. 1 0
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  35. 43 27
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  36. 1 0
      apps/app/src/components/PageHeader/PageTitleHeader.module.scss
  37. 4 5
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  38. 26 8
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  39. 4 0
      apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss
  40. 17 2
      apps/app/src/components/Sidebar/AppTitle/AppTitle.tsx
  41. 5 4
      apps/app/src/pages/admin/slack-integration.page.tsx
  42. 1 0
      apps/app/src/pages/installer.page.tsx
  43. 7 0
      apps/app/src/server/crowi/index.js
  44. 9 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  45. 3 6
      apps/app/src/server/service/socket-io.js
  46. 35 5
      apps/app/src/server/service/yjs-connection-manager.ts
  47. 1 1
      apps/app/src/styles/_fonts.scss
  48. 2 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx
  49. 2 0
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  50. 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);
 }
 
-.admin-bot-card {
-  .grw-botcard-title-active {
-    color: $gray-200;
-  }
-}
-
 /*
  * Form Slider
  */

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

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

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

@@ -108,6 +108,7 @@
   "Error occurred": "Error occurred",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
+  "Input parent page path": "Input parent page path",
   "New Page": "New page",
   "Create under": "Create page under below:",
   "V5 Page Migration": "Convert To V5 Compatibility",

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

@@ -107,6 +107,7 @@
   "Error occurred": "エラーが発生しました",
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
+  "Input parent page path": "親ページのパスを入力",
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",

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

@@ -113,6 +113,7 @@
   "Error occurred": "Error occurred",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
+  "Input parent page path": "Input parent page path",
   "New Page": "新页面",
   "Create under": "Create page under below:",
   "V5 Page Migration": "转换为V5的兼容性",

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

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

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

+ 2 - 2
apps/app/src/components/Admin/App/AppSetting.jsx

@@ -40,7 +40,7 @@ const AppSetting = (props) => {
           <input
             className="form-control"
             type="text"
-            defaultValue={adminAppContainer.state.title || ''}
+            value={adminAppContainer.state.title || ''}
             onChange={(e) => {
               adminAppContainer.changeTitle(e.target.value);
             }}
@@ -60,7 +60,7 @@ const AppSetting = (props) => {
           <input
             className="form-control"
             type="text"
-            defaultValue={adminAppContainer.state.confidential || ''}
+            value={adminAppContainer.state.confidential || ''}
             onChange={(e) => {
               adminAppContainer.changeConfidential(e.target.value);
             }}

+ 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 { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
+import Image from 'next/image';
 
 const botDetails = {
   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 isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
+  const { isActive, botType, onBotTypeSelectHandler } = props;
+
+  const isBotTypeOfficial = botType === SlackbotType.OFFICIAL;
 
   return (
     <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"
-      key={props.botType}
+      key={botType}
     >
       <div>
         <h3 className={`card-header mb-0 py-3
               ${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">
-            {t(`admin:slack_integration.selecting_bot_types.${botDetails[props.botType].botTypeCategory}`)}
+            {t(`admin:slack_integration.selecting_bot_types.${botDetails[botType].botTypeCategory}`)}
           </span>
 
           {/*  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 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>
             )}
 
-          <i className={props.isActive ? 'grw-botcard-title-active' : ''} aria-hidden="true"></i>
+          <i className={isActive ? 'grw-botcard-title-active' : ''} aria-hidden="true"></i>
         </h3>
       </div>
       <div className="card-body p-4">
         <div className="card-text">
           <div className="my-2">
-            <img
+            <Image
               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>
-              <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 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>
-              <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>
   );
-
-};
-
-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 PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
 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>
 );
 
-const BridgeCore = (props) => {
+type BridgeCoreProps = {
+  description: string,
+  iconClass: string,
+  iconName: string,
+  hrClass: string,
+  withProxy?: boolean,
+}
+const BridgeCore = (props: BridgeCoreProps): JSX.Element => {
   const {
     description, iconClass, iconName, hrClass, withProxy,
   } = 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 { 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';

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

@@ -9,8 +9,8 @@ import { useAppTitle } from '~/stores/context';
 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 WithProxyAccordions from './WithProxyAccordions';
 
@@ -93,7 +93,7 @@ const CustomBotWithProxySettings = (props) => {
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
         <a href={t('admin:slack_integration.docs_url.custom_bot_with_proxy')} target="_blank" rel="noopener noreferrer">
-          <span className="growi-custom-icons btn-link ms-2 fs-3">external_link</span>
+          <span className="growi-custom-icons btn-link ms-2">external_link</span>
         </a>
       </h2>
 

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

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

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 import { useAppTitle } from '~/stores/context';
 
-import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
+import { CustomBotWithoutProxyConnectionStatus } from './CustomBotWithoutProxyConnectionStatus';
 import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 const CustomBotWithoutProxySettings = (props) => {
@@ -24,7 +24,7 @@ const CustomBotWithoutProxySettings = (props) => {
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
         <a href={t('admin:slack_integration.docs_url.custom_bot_without_proxy')} target="_blank" rel="noopener noreferrer">
-          <span className="growi-custom-icons btn-link ms-2 fs-3">external_link</span>
+          <span className="growi-custom-icons btn-link ms-2">external_link</span>
         </a>
       </h2>
 

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

@@ -10,7 +10,7 @@ import Accordion from '../Common/Accordion';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
-import { addLogs } from './slak-integration-util';
+import { addLogs } from './slack-integration-util';
 
 
 export const botInstallationStep = {
@@ -78,7 +78,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         <div className="my-5 d-flex flex-column align-items-center">
           <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
             {t('admin:slack_integration.accordion.create_bot')}
-            <span className="growi-custom-icons ms-2"><small>external_link</small></span>
+            <span className="growi-custom-icons ms-2">external_link</span>
           </button>
           <a
             href={t('admin:slack_integration.docs_url.custom_bot_without_proxy_setting')}
@@ -88,7 +88,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             <p className="text-center mt-1">
               <small>
                 {t('admin:slack_integration.accordion.how_to_create_a_bot')}
-                <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
+                <span className="growi-custom-icons ms-2">external_link</span>
               </small>
             </p>
           </a>

+ 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 { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } 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(() => {
     onClickDeleteButton?.();
@@ -19,16 +30,16 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
   }, [onClose]);
 
   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">
         <span>
-          {props.isResetAll && (
+          {isResetAll && (
             <>
               <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset_all_settings')}
             </>
           )}
-          {!props.isResetAll && (
+          {!isResetAll && (
             <>
               <span className="material-symbols-outlined">delete</span>
               {t('admin:slack_integration.delete_slackbot_settings')}
@@ -37,13 +48,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         </span>
       </ModalHeader>
       <ModalBody>
-        {props.isResetAll && (
+        {isResetAll && (
           <span
             // eslint-disable-next-line react/no-danger
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
           />
         )}
-        {!props.isResetAll && (
+        {!isResetAll && (
           <span
             // eslint-disable-next-line react/no-danger
             dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.slackbot_settings_notice') }}
@@ -53,13 +64,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
-          {props.isResetAll && (
+          {isResetAll && (
             <>
               <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset')}
             </>
           )}
-          {!props.isResetAll && (
+          {!isResetAll && (
             <>
               <span className="material-symbols-outlined">delete</span>
               {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';
-
-export default DeleteSlackBotSettingsModal;

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

@@ -11,8 +11,8 @@ import { useAppTitle } from '~/stores/context';
 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 WithProxyAccordions from './WithProxyAccordions';
 
@@ -78,7 +78,7 @@ const OfficialBotSettings = (props) => {
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
         <a href={t('admin:slack_integration.docs_url.official_bot')} target="_blank" rel="noopener noreferrer">
           <span
-            className="growi-custom-icons btn-link ms-2 fs-3"
+            className="growi-custom-icons btn-link ms-2"
             onClick={() => window.open(`${t('admin:slack_integration.docs_url.official_bot')}`, '_blank')}
           >
             external_link

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

@@ -1,4 +1,3 @@
-import React, { FC } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -11,7 +10,7 @@ type Props = {
   onDeleteButtonClicked?: (slackAppIntegration: unknown) => void,
 }
 
-export const SlackAppIntegrationControl: FC<Props> = (props: Props) => {
+export const SlackAppIntegrationControl = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
   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';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
-import BotTypeCard from './BotTypeCard';
+import { BotTypeCard } from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
-import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import { DeleteSlackBotSettingsModal } from './DeleteSlackBotSettingsModal';
 import OfficialBotSettings from './OfficialBotSettings';
 
 
 const botTypes = Object.values(SlackbotType);
 
-const SlackIntegration = () => {
+export const SlackIntegration = (): JSX.Element => {
 
   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 [slackBotToken, setSlackBotToken] = useState(null);
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
@@ -106,12 +106,12 @@ const SlackIntegration = () => {
     fetchSlackIntegrationData();
   }, [fetchSlackIntegrationData]);
 
-  const changeCurrentBotSettings = async(botType) => {
+  const changeCurrentBotSettings = async(botType?: SlackbotType) => {
     try {
       await apiv3Put('/slack-integration-settings/bot-type', {
         currentBotType: botType,
       });
-      setSelectedBotType(null);
+      setSelectedBotType(undefined);
       fetchSlackIntegrationData();
     }
     catch (err) {
@@ -119,7 +119,7 @@ const SlackIntegration = () => {
     }
   };
 
-  const botTypeSelectHandler = async(botType) => {
+  const botTypeSelectHandler = async(botType: SlackbotType) => {
     if (botType === currentBotType) {
       return;
     }
@@ -135,10 +135,10 @@ const SlackIntegration = () => {
   };
 
   const cancelBotChangeHandler = () => {
-    setSelectedBotType(null);
+    setSelectedBotType(undefined);
   };
 
-  let settingsComponent = null;
+  let settingsComponent = <></>;
 
   switch (currentBotType) {
     case SlackbotType.OFFICIAL:
@@ -231,7 +231,7 @@ const SlackIntegration = () => {
           </button>
         </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) => {
             return (
               <div key={botType} className="m-3">
@@ -251,4 +251,4 @@ const SlackIntegration = () => {
   );
 };
 
-export default SlackIntegration;
+SlackIntegration.displayName = 'SlackIntegration';

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

@@ -15,7 +15,7 @@ import Accordion from '../Common/Accordion';
 
 import ManageCommandsProcess from './ManageCommandsProcess';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
-import { addLogs } from './slak-integration-util';
+import { addLogs } from './slack-integration-util';
 
 const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 
@@ -25,7 +25,7 @@ const BotCreateProcess = () => {
     <div className="my-5 d-flex flex-column align-items-center">
       <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
         {t('admin:slack_integration.accordion.create_bot')}
-        <span className="growi-custom-icons ms-2"><small>external_link</small></span>
+        <span className="growi-custom-icons ms-2">external_link</span>
       </button>
       <a
         href={t('admin:slack_integration.docs_url.custom_bot_with_proxy_setting')}
@@ -35,7 +35,7 @@ const BotCreateProcess = () => {
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
-            <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
+            <span className="growi-custom-icons ms-2">external_link</span>
           </small>
         </p>
       </a>
@@ -49,7 +49,7 @@ const BotInstallProcessForOfficialBot = () => {
     <div className="my-5 d-flex flex-column align-items-center">
       <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://slackbot-proxy.growi.org/', '_blank')}>
         {t('admin:slack_integration.accordion.install_now')}
-        <span className="growi-custom-icons ms-2"><small>external_link</small></span>
+        <span className="growi-custom-icons ms-2">external_link</span>
       </button>
       <a
         href={t('admin:slack_integration.docs_url.official_bot_setting')}
@@ -59,7 +59,7 @@ const BotInstallProcessForOfficialBot = () => {
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_install')}
-            <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
+            <span className="growi-custom-icons ms-2">external_link</span>
           </small>
         </p>
       </a>

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

+ 6 - 2
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -13,6 +13,7 @@ type PagePathHierarchicalLinkProps = {
   linkedPagePathByHtml?: LinkedPagePath,
   basePath?: string,
   isInTrash?: boolean,
+  isIconHidden?: boolean,
 
   // !!INTERNAL USE ONLY!!
   isInnerElem?: boolean,
@@ -23,16 +24,18 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
     linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
   } = props;
 
+  const isIconHidden = props.isIconHidden ?? false;
+
   // eslint-disable-next-line react/prop-types
   const RootElm = useCallback(({ children }) => {
     return isInnerElem
       ? <>{children}</>
-      : <span className="text-break">{children}</span>;
+      : <span className="text-break" id="grw-page-path-hierarchical-link">{children}</span>;
   }, [isInnerElem]);
 
   // render root element
   if (linkedPagePath.isRoot) {
-    if (basePath != null) {
+    if (basePath != null || isIconHidden) {
       return <></>;
     }
 
@@ -76,6 +79,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
           basePath={basePath}
           isInTrash={isInTrash || linkedPagePath.isInTrash}
           isInnerElem
+          isIconHidden={isIconHidden}
         />
       ) }
       { isSeparatorRequired && (

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

+ 2 - 2
apps/app/src/components/Me/UISettings.tsx

@@ -57,7 +57,7 @@ export const UISettings = (): JSX.Element => {
               label="Collapsed"
               additionalClasses={styles['grw-sidebar-mode-icon']}
             >
-              <span className="growi-custom-icons">sidebar-collapsed</span>
+              <span className="growi-custom-icons fs-6">sidebar-collapsed</span>
             </IconWithTooltip>
             <div className="form-check form-switch ms-1">
 
@@ -71,7 +71,7 @@ export const UISettings = (): JSX.Element => {
               <label className="form-label form-check-label" htmlFor="swSidebarMode"></label>
             </div>
             <IconWithTooltip id="iwt-sidebar-dock" label="Dock" additionalClasses={styles['grw-sidebar-mode-icon']}>
-              <span className="growi-custom-icons">sidebar-dock</span>
+              <span className="growi-custom-icons fs-6">sidebar-dock</span>
             </IconWithTooltip>
           </div>
           <div className="ms-2">

+ 1 - 0
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -306,6 +306,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
           d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
         `}
         data-testid="grw-contextual-sub-nav"
+        id="grw-contextual-sub-nav"
       >
         {pageId != null && (
           <PageControls

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

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

+ 1 - 1
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -12,7 +12,7 @@ export const EditorNavbar = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
 
   return (
-    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
+    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1 ms-3`}>
       <PageHeader />
       <EditingUserList
         userList={editingUsers?.userList ?? []}

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

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

+ 1 - 1
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -17,7 +17,7 @@ export const PageHeader: FC = () => {
   }
 
   return (
-    <div className={moduleClass}>
+    <div className={`${moduleClass} w-100`}>
       <PagePathHeader
         currentPage={currentPage}
       />

+ 1 - 0
apps/app/src/components/PageHeader/PagePathHeader.module.scss

@@ -1,4 +1,5 @@
 .page-path-header :global {
+  max-width: calc(100vw - 650px);
   input {
     min-width: 20px;
     min-height: unset;

+ 43 - 27
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -1,5 +1,7 @@
-import { useState, useEffect, useCallback } from 'react';
-import type { FC } from 'react';
+import {
+  useState, useEffect, useCallback, memo, useMemo,
+} from 'react';
+import type { CSSProperties, FC } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
@@ -24,7 +26,7 @@ type Props = {
   currentPage: IPagePopulatedToShowRevision
 }
 
-export const PagePathHeader: FC<Props> = (props) => {
+export const PagePathHeader: FC<Props> = memo((props: Props) => {
   const { t } = useTranslation();
   const { currentPage } = props;
 
@@ -37,6 +39,8 @@ export const PagePathHeader: FC<Props> = (props) => {
   const [isHover, setHover] = useState(false);
   const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
 
+  // const [isIconHidden, setIsIconHidden] = useState(false);
+
   const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
   const isOpened = PageSelectModalData?.isOpened ?? false;
 
@@ -71,22 +75,25 @@ export const PagePathHeader: FC<Props> = (props) => {
     setRenameInputShown(true);
   }, [parentPagePath]);
 
-  const clickOutSideHandler = useCallback((e) => {
-    const container = document.getElementById('page-path-header');
-
-    if (container && !container.contains(e.target)) {
-      setRenameInputShown(false);
-    }
-  }, []);
-
-  useEffect(() => {
-    document.addEventListener('click', clickOutSideHandler);
-
-    return () => {
-      document.removeEventListener('click', clickOutSideHandler);
-    };
-  }, [clickOutSideHandler]);
-
+  // TODO: https://redmine.weseek.co.jp/issues/141062
+  // Truncate left side and don't use getElementById
+  //
+  // useEffect(() => {
+  //   const areaElem = document.getElementById('grw-page-path-header-container');
+  //   const linkElem = document.getElementById('grw-page-path-hierarchical-link');
+
+  //   const areaElemWidth = areaElem?.offsetWidth;
+  //   const linkElemWidth = linkElem?.offsetWidth;
+
+  //   if (areaElemWidth && linkElemWidth) {
+  //     setIsIconHidden(linkElemWidth > areaElemWidth);
+  //   }
+  //   else {
+  //     setIsIconHidden(false);
+  //   }
+  // }, [currentPage]);
+  //
+  // const styles: CSSProperties | undefined = isIconHidden ? { direction: 'rtl' } : undefined;
 
   if (dPagePath.isRoot) {
     return <></>;
@@ -95,27 +102,36 @@ export const PagePathHeader: FC<Props> = (props) => {
   return (
     <div
       id="page-path-header"
-      className={`d-flex ${moduleClass} small`}
+      className={`d-flex ${moduleClass} small position-relative`}
       onMouseEnter={() => setHover(true)}
       onMouseLeave={() => setHover(false)}
     >
-      <div className="me-2">
+      <div
+        id="grw-page-path-header-container"
+        className="me-2 d-inline-block overflow-hidden"
+      >
         { isRenameInputShown && (
-          <div className="position-absolute">
+          <div className="position-absolute w-100">
             <ClosableTextInput
-              useAutosizeInput
               value={editingParentPagePath}
-              placeholder={t('Input page name')}
+              placeholder={t('Input parent page path')}
               inputClassName="form-control-sm"
               onPressEnter={onPressEnter}
               onPressEscape={onPressEscape}
               onChange={onInputChange}
               validationTarget={ValidationTarget.PAGE}
+              onClickOutside={onPressEscape}
             />
           </div>
         ) }
-        <div className={`${isRenameInputShown ? 'invisible' : ''}`}>
-          <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />
+        <div
+          className={`${isRenameInputShown ? 'invisible' : ''} text-truncate`}
+          // style={styles}
+        >
+          <PagePathHierarchicalLink
+            linkedPagePath={linkedPagePath}
+            // isIconHidden={isIconHidden}
+          />
         </div>
       </div>
 
@@ -140,4 +156,4 @@ export const PagePathHeader: FC<Props> = (props) => {
       {isOpened && <PageSelectModal />}
     </div>
   );
-};
+});

+ 1 - 0
apps/app/src/components/PageHeader/PageTitleHeader.module.scss

@@ -1,4 +1,5 @@
 .page-title-header :global {
+  max-width: calc(100vw - 650px);
   input {
     min-width: 20px;
     min-height: unset;

+ 4 - 5
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -70,12 +70,11 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
 
   return (
-    <div className={`d-flex align-items-center ${moduleClass} ${props.className ?? ''}`}>
-      <div className="me-1">
+    <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
+      <div className="me-1 d-inline-block overflow-hidden">
         { isRenameInputShown && (
-          <div className="position-absolute">
+          <div className="position-absolute w-100">
             <ClosableTextInput
-              useAutosizeInput
               value={editedPageTitle}
               placeholder={t('Input page name')}
               inputClassName="fs-4"
@@ -87,7 +86,7 @@ export const PageTitleHeader: FC<Props> = (props) => {
             />
           </div>
         ) }
-        <h1 className={`mb-0 fs-4 ${isRenameInputShown ? 'invisible' : ''}`} onClick={onClickPageTitle}>
+        <h1 className={`mb-0 fs-4 ${isRenameInputShown ? 'invisible' : ''} text-truncate`} onClick={onClickPageTitle}>
           {pageTitle}
         </h1>
       </div>

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

@@ -144,11 +144,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
           <span className="label">
             {userRelatedGrantedGroups.length > 1
               ? (
+              // substring for group name truncate
                 <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>
-              ) : userRelatedGrantedGroups[0].name}
+              ) : userRelatedGrantedGroups[0].name.substring(0, 30)}
           </span>
         </span>
       );
@@ -162,7 +163,12 @@ export const GrantSelector = (props: Props): JSX.Element => {
     return (
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
         <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}
           </DropdownToggle>
           <DropdownMenu container={openInModal ? '' : 'body'}>
@@ -209,13 +215,13 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
           return (
             <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"
               key={group.item._id}
               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>}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
@@ -227,6 +233,18 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
   }, [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 (
     <>
       { renderGrantSelector() }
@@ -238,7 +256,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
           toggle={() => setIsSelectGroupModalShown(false)}
           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')}
           </ModalHeader>
           <ModalBody>

+ 4 - 0
apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -22,6 +22,10 @@
       padding: (($height - $logomark-height) / 2) (($width - $logomark-width) / 2);
     }
   }
+
+  .confidential-tooltip {
+    max-width: 180px;
+  }
 }
 
 

+ 17 - 2
apps/app/src/components/Sidebar/AppTitle/AppTitle.tsx

@@ -1,8 +1,9 @@
 import React, { memo } from 'react';
 
 import Link from 'next/link';
+import { UncontrolledTooltip } from 'reactstrap';
 
-import { useAppTitle, useIsDefaultLogo } from '~/stores/context';
+import { useAppTitle, useConfidential, useIsDefaultLogo } from '~/stores/context';
 
 import { SidebarBrandLogo } from '../SidebarBrandLogo';
 
@@ -19,6 +20,7 @@ const AppTitleSubstance = memo((props: Props): JSX.Element => {
 
   const { data: isDefaultLogo } = useIsDefaultLogo();
   const { data: appTitle } = useAppTitle();
+  const { data: confidential } = useConfidential();
 
   return (
     <div className={`${styles['grw-app-title']} ${className} d-flex d-edit-none`}>
@@ -27,12 +29,25 @@ const AppTitleSubstance = memo((props: Props): JSX.Element => {
         <SidebarBrandLogo isDefaultLogo={isDefaultLogo} />
       </Link>
       <div className="flex-grow-1 d-flex align-items-center justify-content-between gap-3 overflow-hidden">
-        <div className="grw-site-name text-truncate">
+        <div id="grw-site-name" className="grw-site-name text-truncate">
           <Link href="/" className="fs-4">
             {appTitle}
           </Link>
         </div>
       </div>
+      {!(confidential == null || confidential === '')
+      && (
+        <UncontrolledTooltip
+          className="d-none d-sm-block confidential-tooltip"
+          innerClassName="text-start"
+          data-testid="confidential-tooltip"
+          placement="top"
+          target="grw-site-name"
+          fade={false}
+        >
+          {confidential}
+        </UncontrolledTooltip>
+      )}
     </div>
   );
 });

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

@@ -1,19 +1,20 @@
-import {
+import type {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 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 { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 
 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 });
 
 

+ 1 - 0
apps/app/src/pages/installer.page.tsx

@@ -47,6 +47,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
         i18n: t('installer.tab'),
       },
       external_accounts: {
+        // TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015
         Icon: () => <span className="growi-custom-icons">external_link</span>,
         Content: DataTransferForm,
         i18n: tCommons('g2g_data_transfer.tab'),

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

@@ -40,8 +40,10 @@ import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
+import { instantiateYjsConnectionManager } from '../service/yjs-connection-manager';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
+
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 
@@ -478,9 +480,14 @@ Crowi.prototype.start = async function() {
 
   // setup terminus
   this.setupTerminus(httpServer);
+
   // attach to socket.io
   this.socketIoService.attachServer(httpServer);
 
+  // Initialization YjsConnectionManager
+  instantiateYjsConnectionManager(this.socketIoService.io);
+  this.socketIoService.setupYjsConnection();
+
   // listen
   const serverListening = httpServer.listen(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 {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
@@ -20,6 +20,7 @@ import {
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { configManager } from '~/server/service/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
+import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
 import Xss from '~/services/xss';
 import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
@@ -79,6 +80,13 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
 
   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
     const parameters = {
       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 YjsConnectionManager from './yjs-connection-manager';
+import { getYjsConnectionManager } from './yjs-connection-manager';
 
 const expressSession = require('express-session');
 const passport = require('passport');
@@ -37,9 +37,6 @@ class SocketIoService {
     });
     this.io.attach(server);
 
-    // create the YjsConnectionManager instance
-    this.yjsConnectionManager = new YjsConnectionManager(this.io);
-
     // create namespace for admin
     this.adminNamespace = this.io.of('/admin');
 
@@ -54,7 +51,6 @@ class SocketIoService {
 
     await this.setupLoginedUserRoomsJoinOnConnection();
     await this.setupDefaultSocketJoinRoomsEventHandler();
-    await this.setupYjsConnection();
   }
 
   getDefaultSocket() {
@@ -160,10 +156,11 @@ class SocketIoService {
   }
 
   setupYjsConnection() {
+    const yjsConnectionManager = getYjsConnectionManager();
     this.io.on('connection', (socket) => {
       socket.on(GlobalSocketEventName.YDocSync, async({ pageId, initialValue }) => {
         try {
-          await this.yjsConnectionManager.handleYDocSync(pageId, initialValue);
+          await yjsConnectionManager.handleYDocSync(pageId, initialValue);
         }
         catch (error) {
           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';
 
-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 {
 
+  private static instance: YjsConnectionManager;
+
   private ysocketio: YSocketIO;
 
   private mdb: MongodbPersistence;
 
-  constructor(io: Server) {
+  private constructor(io: Server) {
     this.ysocketio = new YSocketIO(io);
     this.ysocketio.initialize();
 
@@ -22,8 +24,19 @@ class YjsConnectionManager {
       collectionName: MONGODB_PERSISTENCE_COLLECTION_NAME,
       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> {
@@ -60,6 +73,16 @@ class YjsConnectionManager {
     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 {
     const currentYdoc = this.ysocketio.documents.get(`yjs/${pageId}`);
     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();
+};

+ 1 - 1
apps/app/src/styles/_fonts.scss

@@ -25,7 +25,7 @@
 
 .growi-custom-icons {
   font-family: var(--grw-font-family-custom-icon);
-  font-size: 16px;
+  font-size: 0.8em;
   font-style: normal;
   -webkit-font-smoothing: auto;
   -moz-osx-font-smoothing: auto;

+ 2 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -14,7 +14,8 @@ export const DiagramButton = (props: Props): JSX.Element => {
   }, [editorKey, openDrawioModal]);
   return (
     <button type="button" className="btn btn-toolbar-button" onClick={onClickDiagramButton}>
-      <span className="growi-custom-icons">drawer_io</span>
+      {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
+      <span className="growi-custom-icons fs-6">drawer_io</span>
     </button>
   );
 };

+ 2 - 0
packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -69,6 +69,7 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             <span className="material-symbols-outlined fs-5">format_strikethrough</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('#', true)}>
+            {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">header</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertMarkdownElements('`', '`')}>
@@ -81,6 +82,7 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             <span className="material-symbols-outlined fs-5">format_list_numbered</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('>')}>
+            {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">format_quote</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('- [ ]')}>

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

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