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

Merge branch 'master' into feat/select-unrelated-group-inheritance-on-child-pge-create

Futa Arai 1 год назад
Родитель
Сommit
f053ca3ed4
100 измененных файлов с 768 добавлено и 768 удалено
  1. 26 1
      CHANGELOG.md
  2. 1 1
      apps/app/docker/README.md
  3. 1 1
      apps/app/package.json
  4. 15 8
      apps/app/src/client/services/renderer/renderer.tsx
  5. 0 90
      apps/app/src/client/services/renderer/slide-viewer-renderer.tsx
  6. 1 1
      apps/app/src/components/Admin/App/AppSetting.jsx
  7. 4 5
      apps/app/src/components/Admin/App/FileUploadSetting.tsx
  8. 1 1
      apps/app/src/components/Admin/App/MailSetting.tsx
  9. 8 11
      apps/app/src/components/Admin/App/MaintenanceMode.tsx
  10. 6 6
      apps/app/src/components/Admin/App/QuestionnaireSettings.tsx
  11. 46 50
      apps/app/src/components/Admin/App/SiteUrlSetting.tsx
  12. 1 1
      apps/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  13. 6 6
      apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  14. 2 2
      apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  15. 1 1
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  16. 2 2
      apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  17. 2 2
      apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  18. 2 2
      apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx
  19. 4 3
      apps/app/src/components/Admin/Customize/CustomizeTitle.tsx
  20. 1 1
      apps/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  21. 1 1
      apps/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  22. 8 6
      apps/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  23. 10 9
      apps/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  24. 2 2
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  25. 1 1
      apps/app/src/components/Admin/ExportArchiveDataPage.tsx
  26. 1 1
      apps/app/src/components/Admin/FullTextSearchManagement.tsx
  27. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  28. 2 2
      apps/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  29. 11 11
      apps/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  30. 6 6
      apps/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  31. 6 6
      apps/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  32. 2 2
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx
  33. 4 4
      apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  34. 10 9
      apps/app/src/components/Admin/Notification/GlobalNotification.jsx
  35. 4 4
      apps/app/src/components/Admin/Notification/NotificationSetting.jsx
  36. 16 14
      apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  37. 17 15
      apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  38. 30 30
      apps/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx
  39. 11 11
      apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  40. 30 28
      apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  41. 41 45
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  42. 3 3
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  43. 12 12
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  44. 11 12
      apps/app/src/components/Admin/Security/ShareLinkSetting.tsx
  45. 9 9
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  46. 3 3
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  47. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  48. 1 1
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  49. 18 1
      apps/app/src/components/InstallerForm.tsx
  50. 8 0
      apps/app/src/components/Layout/Admin.module.scss
  51. 3 3
      apps/app/src/components/Layout/AdminLayout.tsx
  52. 13 2
      apps/app/src/components/Page/PageView.tsx
  53. 3 1
      apps/app/src/components/Page/RevisionRenderer.tsx
  54. 26 0
      apps/app/src/components/Page/SlideRenderer.tsx
  55. 2 2
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx
  56. 15 5
      apps/app/src/components/PageEditor/Preview.tsx
  57. 7 4
      apps/app/src/components/PagePresentationModal.tsx
  58. 1 1
      apps/app/src/components/Presentation/Presentation.tsx
  59. 1 1
      apps/app/src/components/Presentation/Slides.tsx
  60. 0 33
      apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx
  61. 18 6
      apps/app/src/components/ShareLinkPageView.tsx
  62. 6 6
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  63. 1 1
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx
  64. 22 23
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/KeycloakGroupSyncSettingsForm.tsx
  65. 23 23
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/LdapGroupSyncSettingsForm.tsx
  66. 3 2
      apps/app/src/features/questionnaire/client/stores/questionnaire.tsx
  67. 10 5
      apps/app/src/server/models/activity.ts
  68. 6 4
      apps/app/src/server/models/in-app-notification.ts
  69. 3 0
      apps/app/src/server/models/page.ts
  70. 2 2
      apps/app/src/server/models/user.js
  71. 12 6
      apps/app/src/server/routes/apiv3/installer.ts
  72. 6 0
      apps/app/src/server/service/config-loader.ts
  73. 28 3
      apps/app/src/server/service/file-uploader/aws.ts
  74. 0 27
      apps/app/src/stores/slide-viewer-renderer.tsx
  75. 1 1
      apps/slackbot-proxy/package.json
  76. 1 1
      package.json
  77. 1 1
      packages/core/package.json
  78. 1 1
      packages/editor/package.json
  79. 15 3
      packages/presentation/package.json
  80. 0 0
      packages/presentation/src/client/components/GrowiSlides.tsx
  81. 0 0
      packages/presentation/src/client/components/MarpSlides.tsx
  82. 0 0
      packages/presentation/src/client/components/Presentation.global.scss
  83. 0 0
      packages/presentation/src/client/components/Presentation.module.scss
  84. 14 18
      packages/presentation/src/client/components/Presentation.tsx
  85. 0 0
      packages/presentation/src/client/components/RichSlideSection.tsx
  86. 0 0
      packages/presentation/src/client/components/Slides.module.scss
  87. 0 0
      packages/presentation/src/client/components/Slides.tsx
  88. 0 0
      packages/presentation/src/client/consts/index.ts
  89. 0 1
      packages/presentation/src/client/index.ts
  90. 0 0
      packages/presentation/src/client/services/growi-marpit.ts
  91. 0 0
      packages/presentation/src/client/services/renderer/extract-sections.ts
  92. 1 0
      packages/presentation/src/services/index.ts
  93. 0 43
      packages/presentation/src/services/parse-slide-frontmatter.ts
  94. 0 89
      packages/presentation/src/services/renderer/slides.ts
  95. 97 0
      packages/presentation/src/services/use-slides-by-frontmatter.ts
  96. 12 1
      packages/presentation/vite.config.ts
  97. 1 1
      packages/preset-templates/package.json
  98. 1 1
      packages/preset-themes/package.json
  99. 1 1
      packages/remark-attachment-refs/package.json
  100. 1 1
      packages/remark-drawio/package.json

+ 26 - 1
CHANGELOG.md

@@ -1,9 +1,34 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.3...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.4...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.0.4](https://github.com/weseek/growi/compare/v7.0.3...v7.0.4) - 2024-05-13
+
+### 💎 Features
+
+* feat: Add a button to open PageCreateModal in the new page creation lead (#8774) @miya
+* feat: Show Yjs status indicator in SubNavigation (#8709) @miya
+* feat: Apply locale to defualt page title (#8600) @jam411
+
+### 🚀 Improvement
+
+* imprv: Behavior when creating first user fails (#8801) @miya
+* imprv: Add indexes for performance (#8800) @yuki-takei
+* imprv: Autosize Input for rename (#8795) @yuki-takei
+* imprv: behavior of dropdown toggle in 'Recent Changes' sidebar (#8782) @maeshinshin
+* imprv: Hide  personal drop down when guest user (#8786) @kazutoweseek
+
+### 🐛 Bug Fixes
+
+* fix: Drawio not available with GROWI slides (#8725) @reiji-h
+* fix: Auto-scroll to the active page in the page tree (#8772) @reiji-h
+
+### 🧰 Maintenance
+
+* ci(deps): bump ejs from 3.1.9 to 3.1.10 (#8784) @dependabot
+
 ## [v7.0.3](https://github.com/weseek/growi/compare/v7.0.2...v7.0.3) - 2024-05-01
 
 ### 🚀 Improvement

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.0.3`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.3/apps/app/docker/Dockerfile)
+* [`7.0.4`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.4/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 1 - 1
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",

+ 15 - 8
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,7 +1,6 @@
 import assert from 'assert';
 
 import { isClient } from '@growi/core/dist/utils/browser-utils';
-import * as slides from '@growi/presentation';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
 // eslint-disable-next-line import/extensions
@@ -19,7 +18,6 @@ import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
@@ -68,7 +66,6 @@ export const generateViewOptions = (
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
-    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -84,7 +81,6 @@ export const generateViewOptions = (
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,
-      slides.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
     )]
@@ -119,7 +115,6 @@ export const generateViewOptions = (
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
-    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -241,6 +236,21 @@ export const generatePresentationViewOptions = (
   // based on simple view options
   const options = generateSimpleViewOptions(config, pagePath);
 
+  const { rehypePlugins } = options;
+
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      addLineNumberAttribute.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    addLineNumberAttribute.rehypePlugin,
+    rehypeSanitizePlugin,
+  );
+
   if (config.isEnabledXssPrevention) {
     verifySanitizePlugin(options, false);
   }
@@ -262,7 +272,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
-    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -281,7 +290,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
-      slides.sanitizeOption,
     )]
     : () => {};
 
@@ -306,7 +314,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
-    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {

+ 0 - 90
apps/app/src/client/services/renderer/slide-viewer-renderer.tsx

@@ -1,90 +0,0 @@
-import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
-import * as drawio from '@growi/remark-drawio';
-// eslint-disable-next-line import/extensions
-import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
-import katex from 'rehype-katex';
-import sanitize from 'rehype-sanitize';
-import math from 'remark-math';
-import deepmerge from 'ts-deepmerge';
-import type { Pluggable } from 'unified';
-
-import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
-import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import * as mermaid from '~/features/mermaid';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import type { RendererOptions } from '~/interfaces/renderer-options';
-import type { RendererConfig } from '~/interfaces/services/renderer';
-import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
-import * as attachment from '~/services/renderer/remark-plugins/attachment';
-import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
-import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
-import {
-  commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
-} from '~/services/renderer/renderer';
-
-
-export const generatePresentationViewOptions = (
-    config: RendererConfig,
-    pagePath: string,
-): RendererOptions => {
-  const options = generateCommonOptions(pagePath);
-
-  const { remarkPlugins, rehypePlugins, components } = options;
-
-  // add remark plugins
-  remarkPlugins.push(
-    math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
-    drawio.remarkPlugin,
-    mermaid.remarkPlugin,
-    xsvToTable.remarkPlugin,
-    attachment.remarkPlugin,
-    lsxGrowiDirective.remarkPlugin,
-    refsGrowiDirective.remarkPlugin,
-  );
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-      drawio.sanitizeOption,
-      mermaid.sanitizeOption,
-      attachment.sanitizeOption,
-      lsxGrowiDirective.sanitizeOption,
-      refsGrowiDirective.sanitizeOption,
-      addLineNumberAttribute.sanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
-    [refsGrowiDirective.rehypePlugin, { pagePath }],
-    rehypeSanitizePlugin,
-    addLineNumberAttribute.rehypePlugin,
-    katex,
-  );
-
-  // add components
-  if (components != null) {
-    components.lsx = lsxGrowiDirective.LsxImmutable;
-    components.ref = refsGrowiDirective.RefImmutable;
-    components.refs = refsGrowiDirective.RefsImmutable;
-    components.refimg = refsGrowiDirective.RefImgImmutable;
-    components.refsimg = refsGrowiDirective.RefsImgImmutable;
-    components.gallery = refsGrowiDirective.GalleryImmutable;
-    components.drawio = drawio.DrawioViewer;
-    components.mermaid = mermaid.MermaidViewer;
-    components.attachment = RichAttachment;
-    components.img = LightBox;
-  }
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};

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

@@ -138,7 +138,7 @@ const AppSetting = (props) => {
         </div>
       </div>
 
-      <div className="row mb-5">
+      <div className="row mb-2">
         <label
           className="text-start text-md-end col-md-3 col-form-label"
         >

+ 4 - 5
apps/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -1,4 +1,5 @@
-import React, { ChangeEvent, useCallback } from 'react';
+import type { ChangeEvent } from 'react';
+import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -30,11 +31,9 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
 
   return (
     <>
-      <p className="card custom-card my-3">
+      <p className="card custom-card bg-warning-subtle my-3">
         {t('admin:app_setting.file_upload')}
-        <br />
-        <br />
-        <span className="text-danger">
+        <span className="text-danger mt-1">
           <span className="material-symbols-outlined">link_off</span>
           {t('admin:app_setting.change_setting')}
         </span>

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

@@ -62,7 +62,7 @@ const MailSetting = (props: Props) => {
         </div>
       </div>
 
-      <div className="row mb-5">
+      <div className="row mb-2">
         <label className="form-label text-start text-md-end col-md-3 col-form-label">
           {t('admin:app_setting.transmission_method')}
         </label>

+ 8 - 11
apps/app/src/components/Admin/App/MaintenanceMode.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -54,21 +55,17 @@ export const MaintenanceMode: FC = () => {
         onConfirm={onConfirmHandler}
         onCancel={() => closeModal()}
       />
-      <p className="card custom-card">
+      <p className="card custom-card bg-warning-subtle">
         {t('admin:maintenance_mode.description')}
-        <br />
-        <br />
-        <span className="text-warning">
+        <span className="text-warning mt-1">
           <span className="material-symbols-outlined">error</span>
           {t('admin:maintenance_mode.supplymentary_message_to_start')}
         </span>
       </p>
-      <div className="row my-3">
-        <div className="mx-auto">
-          <button type="button" className="btn btn-success" onClick={() => openModal()}>
-            {isMaintenanceMode ? t('admin:maintenance_mode.end_maintenance_mode') : t('admin:maintenance_mode.start_maintenance_mode')}
-          </button>
-        </div>
+      <div className="mx-auto my-3">
+        <button type="button" className="btn btn-success" onClick={() => openModal()}>
+          {isMaintenanceMode ? t('admin:maintenance_mode.end_maintenance_mode') : t('admin:maintenance_mode.start_maintenance_mode')}
+        </button>
       </div>
     </div>
   );

+ 6 - 6
apps/app/src/components/Admin/App/QuestionnaireSettings.tsx

@@ -50,8 +50,8 @@ const QuestionnaireSettings = (): JSX.Element => {
 
   return (
     <div id="questionnaire-settings" className="mb-5">
-      <p className="card custom-card">
-        <div className="mb-4">{t('app_setting.questionnaire_settings_explanation')}</div>
+      <p className="card custom-card bg-info-subtle">
+        <div className="mb-3">{t('app_setting.questionnaire_settings_explanation')}</div>
         <span>
           <div className="mb-2">
             <span className="text-info me-2"><span className="material-symbols-outlined">info</span>{t('app_setting.about_data_sent')}</span>
@@ -72,8 +72,8 @@ const QuestionnaireSettings = (): JSX.Element => {
 
       {!isLoading && (
         <>
-          <div className="row my-3">
-            <div className="form-check form-switch form-check-info col-md-5 offset-md-5">
+          <div className="my-4">
+            <div className="form-check form-switch form-check-info">
               <input
                 type="checkbox"
                 className="form-check-input"
@@ -87,8 +87,8 @@ const QuestionnaireSettings = (): JSX.Element => {
             </div>
           </div>
 
-          <div className="row my-4">
-            <div className="form-check form-check-info col-md-5 offset-md-5">
+          <div className="my-4">
+            <div className="form-check form-check-info">
               <input
                 type="checkbox"
                 className="form-check-input"

+ 46 - 50
apps/app/src/components/Admin/App/SiteUrlSetting.tsx

@@ -35,65 +35,61 @@ const SiteUrlSetting = (props: Props) => {
 
   return (
     <React.Fragment>
-      <p className="card custom-card">{t('site_url.desc')}</p>
+      <p className="card custom-card bg-body-tertiary">{t('site_url.desc')}</p>
       {!adminAppContainer.state.isSetSiteUrl
           && (<p className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('site_url.warn')}</p>)}
 
       { adminAppContainer.state.siteUrlUseOnlyEnvVars && (
         <div className="row">
-          <div className="col-md-9 offset-md-3">
-            <p
-              className="alert alert-info"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{
-                __html: t('site_url.note_for_the_only_env_option', { env: 'APP_SITE_URL_USES_ONLY_ENV_VARS' }),
-              }}
-            />
-          </div>
+          <p
+            className="alert alert-info"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{
+              __html: t('site_url.note_for_the_only_env_option', { env: 'APP_SITE_URL_USES_ONLY_ENV_VARS' }),
+            }}
+          />
         </div>
       ) }
 
       <div className="row">
-        <div className="col-md-9 offset-md-3">
-          <table className="table settings-table">
-            <colgroup>
-              <col className="from-db" />
-              <col className="from-env-vars" />
-            </colgroup>
-            <thead>
-              <tr>
-                <th>Database</th>
-                <th>Environment variables</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <td>
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="settingForm[app:siteUrl]"
-                    defaultValue={adminAppContainer.state.siteUrl || ''}
-                    disabled={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
-                    onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
-                    placeholder="e.g. https://my.growi.org"
-                  />
-                  <p className="form-text text-muted">
-                    {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('site_url.help') }} />
-                  </p>
-                </td>
-                <td>
-                  <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
-                  <p className="form-text text-muted">
-                    {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
-                  </p>
-                </td>
-              </tr>
-            </tbody>
-          </table>
-        </div>
+        <table className="table settings-table">
+          <colgroup>
+            <col className="from-db" />
+            <col className="from-env-vars" />
+          </colgroup>
+          <thead>
+            <tr>
+              <th>Database</th>
+              <th>Environment variables</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>
+                <input
+                  className="form-control"
+                  type="text"
+                  name="settingForm[app:siteUrl]"
+                  defaultValue={adminAppContainer.state.siteUrl || ''}
+                  disabled={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
+                  onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
+                  placeholder="e.g. https://my.growi.org"
+                />
+                <p className="form-text text-muted">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('site_url.help') }} />
+                </p>
+              </td>
+              <td>
+                <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
+                <p className="form-text text-muted">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                </p>
+              </td>
+            </tr>
+          </tbody>
+        </table>
       </div>
 
       <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -34,7 +34,7 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_settings.custom_css')}</h2>
 
-          <Card className="card custom-card my-3">
+          <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               { t('admin:customize_settings.write_css') }<br />
               { t('admin:customize_settings.reflect_change') }

+ 6 - 6
apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -37,15 +37,15 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
       <div className="row">
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_settings.function')}</h2>
-          <Card className="card custom-card my-3">
+          <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               {t('admin:customize_settings.function_desc')}
             </CardBody>
           </Card>
 
 
-          <div className="row">
-            <div className="offset-md-3 col-md-6 text-start">
+          <div className="row mt-4">
+            <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isEnabledAttachTitleHeader"
                 label={t('admin:customize_settings.function_options.attach_title_header')}
@@ -89,7 +89,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
           />
 
           <div className="row">
-            <div className="offset-md-3 col-md-6 text-start">
+            <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isEnabledStaleNotification"
                 label={t('admin:customize_settings.function_options.stale_notification')}
@@ -104,7 +104,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="row">
-            <div className="offset-md-3 col-md-6 text-start">
+            <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isAllReplyShown"
                 label={t('admin:customize_settings.function_options.show_all_reply_comments')}
@@ -119,7 +119,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="row">
-            <div className="offset-md-3 col-md-6 text-start">
+            <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
                 optionId="isSearchScopeChildrenAsDefault"
                 label={t('admin:customize_settings.function_options.select_search_scope_children_as_default')}

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -36,7 +36,7 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
 
-          <Card className="card custom-card my-3">
+          <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               <span
                 // eslint-disable-next-line react/no-danger
@@ -47,7 +47,7 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
 
           <div>
             <textarea
-              className="form-control"
+              className="form-control mb-2"
               name="customizeNoscript"
               rows={8}
               defaultValue={adminCustomizeContainer.state.currentCustomizeNoscript || ''}

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -53,7 +53,7 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 target="_blank"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
+              >{`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
               </a>
             </p>
           </CustomizePresentationOption>

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -35,7 +35,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
       <div className="row">
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_settings.custom_script')}</h2>
-          <Card className="card custom-card">
+          <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
               {t('admin:customize_settings.write_java')}<br />
               {t('admin:customize_settings.reflect_change')}
@@ -44,7 +44,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
 
           <div>
             <textarea
-              className="form-control"
+              className="form-control mb-2"
               name="customizeScript"
               rows={8}
               defaultValue={adminCustomizeContainer.state.currentCustomizeScript || ''}

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -42,7 +42,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
 
           <h2 className="admin-setting-header">{t('customize_settings.default_sidebar_mode.title')}</h2>
 
-          <Card className="card custom-card my-3">
+          <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
               {t('customize_settings.default_sidebar_mode.desc')}
             </CardBody>
@@ -79,7 +79,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
             </div>
           </div>
 
-          <Card className="card custom-card my-5">
+          <Card className="card custom-card bg-body-tertiary my-5">
             <CardBody className="px-0 py-2">
               {t('customize_settings.default_sidebar_mode.dock_mode_default_desc')}
             </CardBody>

+ 2 - 2
apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -29,7 +29,7 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
 
       {/* Light and Dark Themes */}
       <div>
-        <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
+        <h3 className="mb-3">{t('customize_settings.theme_desc.light_and_dark')}</h3>
         <div className="hstack gap-3">
           {lightNDarkThemes.map((theme) => {
             return (
@@ -46,7 +46,7 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
 
       {/* Only one mode Theme */}
       <div className="mt-3">
-        <h3>{t('customize_settings.theme_desc.unique')}</h3>
+        <h3 className="mb-3">{t('customize_settings.theme_desc.unique')}</h3>
         <div className="hstack gap-3">
           {oneModeThemes.map((theme) => {
             return (

+ 4 - 3
apps/app/src/components/Admin/Customize/CustomizeTitle.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
@@ -37,7 +38,7 @@ export const CustomizeTitle: FC = () => {
         </div>
 
         <div className="col-12">
-          <Card className="card custom-card">
+          <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
               {/* eslint-disable react/no-danger */}
               <p dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail') }} />
@@ -58,7 +59,7 @@ export const CustomizeTitle: FC = () => {
         </div>
 
         {/* TODO i18n */}
-        <div className="form-text text-muted col-12">
+        <div className="form-text text-muted col-12 mb-3">
           Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
           <br />
           Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>

+ 1 - 1
apps/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx

@@ -18,7 +18,7 @@ const PagingSizeUncontrolledDropdown = (props) => {
   return (
     <React.Fragment>
       <div className="row">
-        <div className="offset-md-3 col-md-6 text-start">
+        <div className="offset-md-2 col-md-7 text-start">
           <div className="my-0 w-100">
             <label className="form-label">{props.label}</label>
           </div>

+ 1 - 1
apps/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -97,7 +97,7 @@ const ElasticsearchManagement = () => {
       socket.off(SocketEventName.FinishAddPage);
       socket.off(SocketEventName.RebuildingFailed);
     };
-  }, [socket]);
+  }, [retrieveIndicesStatus, socket]);
 
 
   const reconnect = async() => {

+ 8 - 6
apps/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -71,12 +71,14 @@ class RebuildIndexControls extends React.Component {
     const header = isRebuildingCompleted ? getCompletedLabel() : getSkipLabel();
 
     return (
-      <LabeledProgressBar
-        header={header}
-        currentCount={current}
-        errorsCount={skip}
-        totalCount={total}
-      />
+      <div className="mb-3">
+        <LabeledProgressBar
+          header={header}
+          currentCount={current}
+          errorsCount={skip}
+          totalCount={total}
+        />
+      </div>
     );
   }
 

+ 10 - 9
apps/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
 class StatusTable extends React.PureComponent {
 
   renderPreInitializedLabel() {
-    return <span className="badge rounded-pill bg-default">――</span>;
+    return <span className="badge text-bg-default">――</span>;
   }
 
   renderConnectionStatusLabels() {
@@ -17,13 +17,13 @@ class StatusTable extends React.PureComponent {
     } = this.props;
 
     const errorOccuredLabel = isErrorOccuredOnSearchService
-      ? <span className="badge rounded-pill bg-danger ms-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
+      ? <span className="badge text-bg-danger ms-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
       : null;
 
     let connectionStatusLabel = null;
     if (!isConfigured) {
       connectionStatusLabel = (
-        <span className="badge rounded-pill bg-default">
+        <span className="badge text-bg-default">
           { t('full_text_search_management.connection_status_label_unconfigured') }
         </span>
       );
@@ -31,8 +31,8 @@ class StatusTable extends React.PureComponent {
     else {
       connectionStatusLabel = isConnected
         // eslint-disable-next-line max-len
-        ? <span data-testid="connection-status-badge-connected" className="badge rounded-pill bg-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
-        : <span className="badge rounded-pill bg-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
+        ? <span data-testid="connection-status-badge-connected" className="badge text-bg-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
+        : <span className="badge text-bg-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
     }
 
     return (
@@ -46,8 +46,8 @@ class StatusTable extends React.PureComponent {
     const { t, isNormalized } = this.props;
 
     return isNormalized
-      ? <span className="badge rounded-pill bg-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
-      : <span className="badge rounded-pill bg-warning text-dark">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
+      ? <span className="badge text-bg-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
+      : <span className="badge text-bg-warning">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
   }
 
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
@@ -55,8 +55,9 @@ class StatusTable extends React.PureComponent {
 
     const aliasLabels = aliases.map((aliasName) => {
       return (
-        <span key={`badge-${indexName}-${aliasName}`} className="badge rounded-pill bg-primary me-2">
-          <span className="material-symbols-outlined">sell</span> {aliasName}
+        <span key={`badge-${indexName}-${aliasName}`} className="badge text-bg-primary me-2">
+          <span className="material-symbols-outlined">sell</span>
+          <span>{aliasName}</span>
         </span>
       );
     });

+ 2 - 2
apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -103,7 +103,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     const html = t('admin:export_management.desc_password_seed');
 
     // eslint-disable-next-line react/no-danger
-    return <div className="card custom-card" dangerouslySetInnerHTML={{ __html: html }}></div>;
+    return <div className="card custom-card bg-body-tertiary" dangerouslySetInnerHTML={{ __html: html }}></div>;
   }, [selectedCollections, t]);
 
   const renderCheckboxes = useCallback((collectionNames, color?) => {
@@ -156,7 +156,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
   }, [isAllChecked, checkAll]);
 
   return (
-    <Modal isOpen={isOpen} toggle={onClose}>
+    <Modal size="lg" isOpen={isOpen} toggle={onClose}>
       <ModalHeader tag="h4" toggle={onClose} className="text-info">
         {t('admin:export_management.export_collections')}
       </ModalHeader>

+ 1 - 1
apps/app/src/components/Admin/ExportArchiveDataPage.tsx

@@ -151,7 +151,7 @@ const ExportArchiveDataPage = (): JSX.Element => {
       ) }
 
       <div className="mt-5">
-        <h3>{t('export_management.exported_data_list')}</h3>
+        <h3 className="mb-3">{t('export_management.exported_data_list')}</h3>
         <ArchiveFilesTable
           zipFileStats={zipFileStats}
           onZipFileStatRemove={onZipFileStatRemove}

+ 1 - 1
apps/app/src/components/Admin/FullTextSearchManagement.tsx

@@ -9,7 +9,7 @@ export const FullTextSearchManagement = (): JSX.Element => {
 
   return (
     <div data-testid="admin-full-text-search">
-      <h2> { t('full_text_search_management.elasticsearch_management') } </h2>
+      <h2 className="mb-4"> { t('full_text_search_management.elasticsearch_management') } </h2>
       <ElasticsearchManagement />
     </div>
   );

+ 1 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -75,7 +75,7 @@ class UploadForm extends React.Component {
             </div>
           </div>
           <div className="row">
-            <div className="mx-auto">
+            <div className="mx-auto mt-3">
               <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
                 {t('admin:importer_management.growi_settings.upload')}
               </button>

+ 2 - 2
apps/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -92,8 +92,8 @@ class GrowiArchiveSection extends React.Component {
 
     return (
       <Fragment>
-        <h2>{t('importer_management.import_growi_archive')}</h2>
-        <div className="card custom-card mb-4 small">
+        <h2 className="mb-3">{t('importer_management.import_growi_archive')}</h2>
+        <div className="card custom-card bg-body-tertiary mb-4 small">
           <ul>
             <li>{t('importer_management.skip_username_and_email_when_overlapped')}</li>
             <li>{t('importer_management.prepare_new_account_for_migration')}</li>

+ 11 - 11
apps/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -57,13 +57,13 @@ class ImportDataPageContents extends React.Component {
               </tbody>
             </table>
 
-            <div className="card custom-card mb-0 small">
+            <div className="card custom-card bg-body-tertiary mb-0 small">
               <ul>
                 <li>{t('importer_management.page_skip')}</li>
               </ul>
             </div>
 
-            <div className="row">
+            <div className="row mt-4">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </div>
 
@@ -83,7 +83,7 @@ class ImportDataPageContents extends React.Component {
 
             </div>
 
-            <div className="row">
+            <div className="row mt-3">
               <label htmlFor="settingForm[importer:esa:access_token]" className="text-start text-md-end col-md-3 col-form-label">
                 {t('importer_management.esa_settings.access_token')}
               </label>
@@ -98,12 +98,12 @@ class ImportDataPageContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row mt-3">
               <div className="offset-md-3 col-md-6">
                 <input
                   id="testConnectionToEsa"
                   type="button"
-                  className="btn btn-primary btn-esa"
+                  className="btn btn-primary btn-esa me-3"
                   name="Esa"
                   onClick={adminImportContainer.esaHandleSubmit}
                   value={t('importer_management.import')}
@@ -163,16 +163,16 @@ class ImportDataPageContents extends React.Component {
                 </tr>
               </tbody>
             </table>
-            <div className="card custom-card mb-0 small">
+            <div className="card custom-card bg-body-tertiary mb-3 small">
               <ul>
                 <li>{t('importer_management.page_skip')}</li>
               </ul>
             </div>
 
-            <div className="row">
+            <div className="row mt-3">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </div>
-            <div className="row">
+            <div className="row mt-3">
               <label htmlFor="settingForm[importer:qiita:team_name]" className="text-start text-md-end col-md-3 col-form-label">
                 {t('importer_management.qiita_settings.team_name')}
               </label>
@@ -187,7 +187,7 @@ class ImportDataPageContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row mt-3">
               <label htmlFor="settingForm[importer:qiita:access_token]" className="text-start text-md-end col-md-3 col-form-label">
                 {t('importer_management.qiita_settings.access_token')}
               </label>
@@ -203,12 +203,12 @@ class ImportDataPageContents extends React.Component {
             </div>
 
 
-            <div className="row">
+            <div className="row mt-3">
               <div className="offset-md-3 col-md-6">
                 <input
                   id="testConnectionToQiita"
                   type="button"
-                  className="btn btn-primary btn-qiita"
+                  className="btn btn-primary btn-qiita me-3"
                   name="Qiita"
                   onClick={adminImportContainer.qiitaHandleSubmit}
                   value={t('importer_management.import')}

+ 6 - 6
apps/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -99,9 +99,9 @@ class SlackConfiguration extends React.Component {
         )
           : (
             <React.Fragment>
-              <h2 className="border-bottom mb-5">{t('notification_settings.slack_app_configuration')}</h2>
+              <h2 className="border-bottom mb-3">{t('notification_settings.slack_app_configuration')}</h2>
 
-              <div className="card custom-card">
+              <div className="card custom-card bg-danger-subtle">
                 <span className="text-danger"><span className="material-symbols-outlined">error</span>NOT RECOMMENDED</span>
                 <br />
                 {/* eslint-disable-next-line react/no-danger */}
@@ -116,7 +116,7 @@ class SlackConfiguration extends React.Component {
                 </a>
               </div>
 
-              <div className="row mb-5">
+              <div className="row mb-5 mt-4">
                 <label className="form-label col-md-3 text-start text-md-end">OAuth access token</label>
                 <div className="col-md-6">
                   <input
@@ -144,8 +144,8 @@ class SlackConfiguration extends React.Component {
           <a href="#collapseHelpForIwh" data-bs-toggle="collapse">{t('notification_settings.how_to.header')}</a>
         </h3>
 
-        <ol id="collapseHelpForIwh" className="collapse">
-          <li>
+        <ol id="collapseHelpForIwh" className="collapse card custom-card bg-body-tertiary">
+          <li className="ms-3">
             {t('notification_settings.how_to.workspace')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
@@ -154,7 +154,7 @@ class SlackConfiguration extends React.Component {
               <li>{t('notification_settings.how_to.workspace_desc3')}</li>
             </ol>
           </li>
-          <li>
+          <li className="ms-3">
             {t('notification_settings.how_to.at_growi')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}

+ 6 - 6
apps/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -40,24 +40,24 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   }, [adminMarkDownContainer]);
 
   return (
-    <div data-testid="admin-markdown">
+    <div data-testid="admin-markdown" className="mb-5">
       {/* Line Break Setting */}
       <h2 className="admin-setting-header">{t('markdown_settings.lineBreak_header')}</h2>
-      <Card className="card custom-card my-3">
+      <Card className="card custom-card bg-body-tertiary my-3">
         <CardBody className="px-0 py-2">{ t('markdown_settings.lineBreak_desc') }</CardBody>
       </Card>
       <LineBreakForm />
 
       {/* Indent Setting */}
-      <h2 className="admin-setting-header">{t('markdown_settings.indent_header')}</h2>
-      <Card className="card custom-card my-3">
+      <h2 className="admin-setting-header mt-5">{t('markdown_settings.indent_header')}</h2>
+      <Card className="card custom-card bg-body-tertiary my-3">
         <CardBody className="px-0 py-2">{t('markdown_settings.indent_desc') }</CardBody>
       </Card>
       <IndentForm />
 
       {/* XSS Setting */}
-      <h2 className="admin-setting-header">{ t('markdown_settings.xss_header') }</h2>
-      <Card className="card custom-card my-3">
+      <h2 className="admin-setting-header mt-5">{ t('markdown_settings.xss_header') }</h2>
+      <Card className="card custom-card bg-body-tertiary my-3">
         <CardBody className="px-0 py-2">{ t('markdown_settings.xss_desc') }</CardBody>
       </Card>
       <XssForm />

+ 2 - 2
apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx

@@ -41,7 +41,7 @@ class WhitelistInput extends React.Component {
         <div className="mt-4">
           <div className="d-flex justify-content-between">
             {t('markdown_settings.xss_options.tag_names')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendTagButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendTagButton}>
               {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
             </p>
           </div>
@@ -58,7 +58,7 @@ class WhitelistInput extends React.Component {
         <div className="mt-4">
           <div className="d-flex justify-content-between">
             {t('markdown_settings.xss_options.tag_attributes')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendAttrButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendAttrButton}>
               {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
             </p>
           </div>

+ 4 - 4
apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -45,10 +45,10 @@ class XssForm extends React.Component {
     const rehypeRecommendedAttributes = JSON.stringify(sanitizeDefaultSchema.attributes);
 
     return (
-      <div className="col-12 my-3">
+      <div className="col-12 mt-3">
         <div className="row">
 
-          <div className="col-md-6 col-sm-12 align-self-start mb-4">
+          <div className="col-md-6 col-sm-12 align-self-start">
             <div className="form-check">
               <input
                 type="radio"
@@ -90,7 +90,7 @@ class XssForm extends React.Component {
             </div>
           </div>
 
-          <div className="col-md-6 col-sm-12 align-self-start mb-4">
+          <div className="col-md-6 col-sm-12 align-self-start">
             <div className="form-check">
               <input
                 type="radio"
@@ -119,7 +119,7 @@ class XssForm extends React.Component {
       <React.Fragment>
         <fieldset className="col-12">
           <div>
-            <div className="col-8 offset-4 my-3">
+            <div className="col-8 offset-4 mt-3">
               <div className="form-check form-switch form-check-success">
                 <input
                   type="checkbox"

+ 10 - 9
apps/app/src/components/Admin/Notification/GlobalNotification.jsx

@@ -36,7 +36,7 @@ const GlobalNotification = (props) => {
     <>
       <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
 
-      <p className="card custom-card">
+      <p className="card custom-card bg-body-tertiary">
         {/* eslint-disable-next-line react/no-danger */}
         <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
       </p>
@@ -75,7 +75,7 @@ const GlobalNotification = (props) => {
           </div>
         </div>
       </div>
-      <div className="row my-3">
+      <div className="row mt-3 mb-5">
         <div className="col-sm-5 offset-sm-4">
           <button
             type="button"
@@ -87,14 +87,15 @@ const GlobalNotification = (props) => {
         </div>
       </div>
 
-      <h2 className="border-bottom mb-5">{t('notification_settings.notification_list')}
-        <button
-          className="btn btn-outline-secondary pull-right"
-          type="button"
-          onClick={() => router.push('/admin/global-notification/new')}
-        >{t('notification_settings.add_notification')}
-        </button>
+      <h2 className="border-bottom mb-3">
+        {t('notification_settings.notification_list')}
       </h2>
+      <button
+        className="btn btn-outline-secondary mb-3"
+        type="button"
+        onClick={() => router.push('/admin/global-notification/new')}
+      >{t('notification_settings.add_notification')}
+      </button>
       <table className="table table-bordered">
         <thead>
           <tr>

+ 4 - 4
apps/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -31,14 +31,14 @@ const Badge = ({ isEnabled }) => {
   const { t } = useTranslation('admin');
 
   return isEnabled
-    ? <span className="badge bg-success">{t('external_notification.enabled')}</span>
-    : <span className="badge bg-primary">{t('external_notification.disabled')}</span>;
+    ? <span className="badge text-bg-success">{t('external_notification.enabled')}</span>
+    : <span className="badge text-bg-primary">{t('external_notification.disabled')}</span>;
 };
 
 const SkeletonListItem = () => (
   <li className="list-group-item">
     <h4 className="mb-2">
-      <span className="badge bg-primary">――</span>
+      <span className="badge text-bg-primary">――</span>
       <span className="ms-2">...</span>
     </h4>
   </li>
@@ -51,7 +51,7 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
 
   return (
-    <li data-testid="slack-integration-list-item" className="list-group-item">
+    <li data-testid="slack-integration-list-item" className="list-group-item bg-body-tertiary">
       <h4>
         <Badge isEnabled={isEnabled} />
         <a href="/admin/slack-integration" className="ms-2">{t('slack_integration.slack_integration')}</a>

+ 16 - 14
apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -56,7 +56,7 @@ class GitHubSecurityManagementContents extends React.Component {
           </div>
         )}
 
-        <div className="row">
+        <div className="row my-4">
           <div className="col-6 offset-3">
             <div className="form-check form-switch form-check-success">
               <input
@@ -71,11 +71,11 @@ class GitHubSecurityManagementContents extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>}
+              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
-        <div className="row mb-5">
+        <div className="row mb-4">
           <label className="form-label col-12 col-md-3 text-start text-md-end py-2">{t('security_settings.callback_URL')}</label>
           <div className="col-12 col-md-6">
             <input
@@ -100,9 +100,9 @@ class GitHubSecurityManagementContents extends React.Component {
         {isGitHubEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="githubClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
               <div className="col-6">
                 <input
@@ -118,7 +118,7 @@ class GitHubSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-3">
               <label htmlFor="githubClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
               <div className="col-6">
                 <input
@@ -134,7 +134,7 @@ class GitHubSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-3">
               <div className="offset-3 col-6 text-start">
                 <div className="form-check form-check-success">
                   <input
@@ -156,7 +156,7 @@ class GitHubSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row my-3">
+            <div className="row mb-4">
               <div className="offset-3 col-5">
                 <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                   {t('Update')}
@@ -174,12 +174,14 @@ class GitHubSecurityManagementContents extends React.Component {
             <span className="material-symbols-outlined" aria-hidden="true">help</span>
             <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
           </h4>
-          <ol id="collapseHelpForGitHubOauth" className="collapse">
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_3') }} />
-          </ol>
+          <div className="card custom-card bg-body-tertiary">
+            <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
+              {/* eslint-disable-next-line max-len */}
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_3') }} />
+            </ol>
+          </div>
         </div>
 
       </React.Fragment>

+ 17 - 15
apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -54,7 +54,7 @@ class GoogleSecurityManagementContents extends React.Component {
           </div>
         )}
 
-        <div className="row">
+        <div className="row my-4">
           <div className="col-6 offset-3">
             <div className="form-check form-switch form-check-success">
               <input
@@ -69,7 +69,7 @@ class GoogleSecurityManagementContents extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
-              && <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>}
+              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -99,9 +99,9 @@ class GoogleSecurityManagementContents extends React.Component {
         {isGoogleEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="googleClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
               <div className="col-6">
                 <input
@@ -117,7 +117,7 @@ class GoogleSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="googleClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
               <div className="col-6">
                 <input
@@ -133,7 +133,7 @@ class GoogleSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-3">
               <div className="offset-3 col-6">
                 <div className="form-check form-check-success">
                   <input
@@ -155,7 +155,7 @@ class GoogleSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row my-3">
+            <div className="row mb-4">
               <div className="offset-3 col-5">
                 <button
                   type="button"
@@ -178,14 +178,16 @@ class GoogleSecurityManagementContents extends React.Component {
             <span className="material-symbols-outlined" aria-hidden="true">help</span>
             <a href="#collapseHelpForGoogleOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.google')}</a>
           </h4>
-          <ol id="collapseHelpForGoogleOauth" className="collapse">
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_2') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_3') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_5') }} />
-          </ol>
+          <div className="card custom-card bg-body-tertiary">
+            <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
+              {/* eslint-disable-next-line max-len */}
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_2') }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_3') }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_5') }} />
+            </ol>
+          </div>
         </div>
 
       </React.Fragment>

+ 30 - 30
apps/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx

@@ -54,11 +54,11 @@ class LdapSecuritySettingContents extends React.Component {
     return (
       <React.Fragment>
 
-        <h2 className="alert-anchor border-bottom">
+        <h2 className="alert-anchor border-bottom mb-4">
           LDAP
         </h2>
 
-        <div className="row">
+        <div className="row my-4">
           <div className="col-6 offset-3">
             <div className="form-check form-switch form-check-success">
               <input
@@ -73,7 +73,7 @@ class LdapSecuritySettingContents extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
-              && <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>}
+              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -81,13 +81,13 @@ class LdapSecuritySettingContents extends React.Component {
         {isLdapEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
 
-            <div className="row">
+            <div className="row my-3">
               <label htmlFor="serverUrl" className="text-start text-md-end col-md-3 col-form-label">
                 Server URL
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -106,11 +106,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong>{t('security_settings.ldap.bind_mode')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <div className="dropdown">
                   <button
                     className="btn btn-outline-secondary dropdown-toggle"
@@ -136,11 +136,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong>Bind DN</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -171,11 +171,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <div htmlFor="bindDNPassword" className="text-start text-md-end col-md-3 col-form-label">
                 <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
               </div>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 {(adminLdapSecurityContainer.state.isUserBind) ? (
                   <p className="card custom-card passport-ldap-userbind">
                     <small>
@@ -202,11 +202,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong>{t('security_settings.ldap.search_filter')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -234,15 +234,15 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mb-4">
               Attribute Mapping ({t('optional')})
             </h3>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="attrMapUsername">{t('username')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -258,8 +258,8 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
-              <div className="offset-md-3 col-md-6">
+            <div className="row my-3">
+              <div className="offset-md-3 col-md-9">
                 <div className="form-check form-check-success">
                   <input
                     type="checkbox"
@@ -282,11 +282,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="attrMapMail">{t('Email')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -303,11 +303,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="attrMapName">{t('Name')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -324,15 +324,15 @@ class LdapSecuritySettingContents extends React.Component {
             </div>
 
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mb-4">
               {t('security_settings.ldap.group_search_filter')} ({t('optional')})
             </h3>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="groupSearchBase">{t('security_settings.ldap.group_search_base_DN')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -350,11 +350,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="groupSearchFilter">{t('security_settings.ldap.group_search_filter')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"
@@ -381,11 +381,11 @@ class LdapSecuritySettingContents extends React.Component {
               </div>
             </div>
 
-            <div className="row">
+            <div className="row my-3">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">
                 <strong htmlFor="groupDnProperty">{t('security_settings.ldap.group_search_user_DN_property')}</strong>
               </label>
-              <div className="col-md-6">
+              <div className="col-md-9">
                 <input
                   className="form-control"
                   type="text"

+ 11 - 11
apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -62,7 +62,7 @@ class LocalSecuritySettingContents extends React.Component {
           />
         )}
 
-        <div className="row mb-5">
+        <div className="row mt-4 mb-5">
           <div className="col-6 offset-3">
             <div className="form-check form-switch form-check-success">
               <input
@@ -88,10 +88,10 @@ class LocalSecuritySettingContents extends React.Component {
             <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row">
-              <div className="col-12 col-md-3 text-start text-md-end py-2">
+              <div className="col-12 col-md-4 text-start text-md-end py-2">
                 <strong>{t('security_settings.register_limitation')}</strong>
               </div>
-              <div className="col-12 col-md-6">
+              <div className="col-12 col-md-8">
                 <div className="dropdown">
                   <button
                     className="btn btn-outline-secondary dropdown-toggle"
@@ -139,10 +139,10 @@ class LocalSecuritySettingContents extends React.Component {
               </div>
             </div>
             <div className="row">
-              <div className="col-12 col-md-3 text-start text-md-end">
+              <div className="col-12 col-md-4 text-start text-md-end">
                 <strong dangerouslySetInnerHTML={{ __html: t('security_settings.The whitelist of registration permission E-mail address') }} />
               </div>
-              <div className="col-12 col-md-6">
+              <div className="col-12 col-md-8">
                 <textarea
                   className="form-control"
                   type="textarea"
@@ -163,8 +163,8 @@ class LocalSecuritySettingContents extends React.Component {
             </div>
 
             <div className="row">
-              <label className="col-12 col-md-3 text-start text-md-end  col-form-label">{t('security_settings.Local.password_reset_by_users')}</label>
-              <div className="col-12 col-md-6">
+              <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.password_reset_by_users')}</label>
+              <div className="col-12 col-md-8">
                 <div className="form-check form-switch form-check-success">
                   <input
                     type="checkbox"
@@ -178,7 +178,7 @@ class LocalSecuritySettingContents extends React.Component {
                   </label>
                 </div>
                 {!isMailerSetup && (
-                  <div className="alert alert-warning p-1 my-1 small d-inline-block">
+                  <div className="alert alert-warning p-2 my-1 small d-inline-block">
                     <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
                       <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
@@ -192,8 +192,8 @@ class LocalSecuritySettingContents extends React.Component {
             </div>
 
             <div className="row">
-              <label className="col-12 col-md-3 text-start text-md-end  col-form-label">{t('security_settings.Local.email_authentication')}</label>
-              <div className="col-12 col-md-6">
+              <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.email_authentication')}</label>
+              <div className="col-12 col-md-8">
                 <div className="form-check form-switch form-check-success">
                   <input
                     type="checkbox"
@@ -207,7 +207,7 @@ class LocalSecuritySettingContents extends React.Component {
                   </label>
                 </div>
                 {!isMailerSetup && (
-                  <div className="alert alert-warning p-1 my-1 small d-inline-block">
+                  <div className="alert alert-warning p-2 my-1 small d-inline-block">
                     <span>{t('commons:alert.please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
                       <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}

+ 30 - 28
apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -48,7 +48,7 @@ class OidcSecurityManagementContents extends React.Component {
           {t('security_settings.OAuth.OIDC.name')}
         </h2>
 
-        <div className="row mb-5">
+        <div className="row  my-4">
           <div className="offset-3 col-6">
             <div className="form-check form-switch form-check-success">
               <input
@@ -63,7 +63,7 @@ class OidcSecurityManagementContents extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
-              && <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>}
+              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -92,9 +92,9 @@ class OidcSecurityManagementContents extends React.Component {
         {isOidcEnabled && (
           <>
 
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcProviderName" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.providerName')}</label>
               <div className="col-md-6">
                 <input
@@ -107,7 +107,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcIssuerHost" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.issuerHost')}</label>
               <div className="col-md-6">
                 <input
@@ -123,7 +123,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcClientId" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.clientID')}</label>
               <div className="col-md-6">
                 <input
@@ -139,7 +139,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcClientSecret" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.client_secret')}</label>
               <div className="col-md-6">
                 <input
@@ -155,7 +155,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcAuthorizationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.authorization_endpoint')}
               </label>
@@ -173,7 +173,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcTokenEndpoint" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.token_endpoint')}</label>
               <div className="col-md-6">
                 <input
@@ -189,7 +189,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcRevocationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.revocation_endpoint')}
               </label>
@@ -207,7 +207,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcIntrospectionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.introspection_endpoint')}
               </label>
@@ -225,7 +225,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcUserInfoEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.userinfo_endpoint')}
               </label>
@@ -243,7 +243,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcEndSessionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.end_session_endpoint')}
               </label>
@@ -261,7 +261,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcRegistrationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
                 {t('security_settings.registration_endpoint')}
               </label>
@@ -279,7 +279,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcJWKSUri" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.jwks_uri')}</label>
               <div className="col-md-6">
                 <input
@@ -295,11 +295,11 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mb-4">
               Attribute Mapping ({t('optional')})
             </h3>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcAttrMapId" className="text-start text-md-end col-md-3 col-form-label">Identifier</label>
               <div className="col-md-6">
                 <input
@@ -315,7 +315,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcAttrMapUserName" className="text-start text-md-end col-md-3 col-form-label">{t('username')}</label>
               <div className="col-md-6">
                 <input
@@ -331,7 +331,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcAttrMapName" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
               <div className="col-md-6">
                 <input
@@ -347,7 +347,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label htmlFor="oidcAttrMapEmail" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
               <div className="col-md-6">
                 <input
@@ -363,7 +363,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <label className="form-label text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
               <div className="col-md-6">
                 <input
@@ -385,7 +385,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <div className="offset-md-3 col-md-6">
                 <div className="form-check form-check-success">
                   <input
@@ -407,7 +407,7 @@ class OidcSecurityManagementContents extends React.Component {
               </div>
             </div>
 
-            <div className="row mb-5">
+            <div className="row mb-4">
               <div className="offset-md-3 col-md-6">
                 <div className="form-check form-check-success">
                   <input
@@ -452,11 +452,13 @@ class OidcSecurityManagementContents extends React.Component {
             <span className="material-symbols-outlined" aria-hidden="true">help</span>
             <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
           </h4>
-          <ol id="collapseHelpForOidcOauth" className="collapse">
-            <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
-            <li>{t('security_settings.OAuth.OIDC.register_2')}</li>
-            <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
-          </ol>
+          <div className=" card custom-card bg-body-tertiary">
+            <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
+              <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
+              <li>{t('security_settings.OAuth.OIDC.register_2')}</li>
+              <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
+            </ol>
+          </div>
         </div>
 
       </>

+ 41 - 45
apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -64,7 +64,7 @@ class SamlSecurityManagementContents extends React.Component {
           />
         )}
 
-        <div className="row mb-5">
+        <div className="row mt-4 mb-5">
           <div className="col-6 offset-3">
             <div className="form-check form-switch form-check-success">
               <input
@@ -80,7 +80,7 @@ class SamlSecurityManagementContents extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
-              && <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>}
+              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -112,7 +112,7 @@ class SamlSecurityManagementContents extends React.Component {
             {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
               <div className="alert alert-danger">
                 {t('security_settings.missing mandatory configs')}
-                <ul>
+                <ul className="mb-0">
                   {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
                     const key = configKey.replace('security:passport-saml:', '');
                     return <li key={configKey}>{t(`security_settings.form_item_name.${key}`)}</li>;
@@ -122,7 +122,7 @@ class SamlSecurityManagementContents extends React.Component {
             )}
 
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mb-3">
               Basic Settings
             </h3>
 
@@ -232,7 +232,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               </tbody>
             </table>
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mt-5 mb-3">
               Attribute Mapping
             </h3>
 
@@ -386,55 +386,51 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               </tbody>
             </table>
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mt-5 mb-4">
               Attribute Mapping Options
             </h3>
 
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6 text-start">
-                <div className="form-check form-check-success">
-                  <input
-                    id="bindByUserName-SAML"
-                    className="form-check-input"
-                    type="checkbox"
-                    checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="form-label form-check-label"
-                    htmlFor="bindByUserName-SAML"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
-                </p>
+            <div className="row ms-3">
+              <div className="form-check form-check-success">
+                <input
+                  id="bindByUserName-SAML"
+                  className="form-check-input"
+                  type="checkbox"
+                  checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                  onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                />
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="bindByUserName-SAML"
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
+                />
               </div>
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+              </p>
             </div>
 
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6 text-start">
-                <div className="form-check form-check-success">
-                  <input
-                    id="bindByEmail-SAML"
-                    className="form-check-input"
-                    type="checkbox"
-                    checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                    onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="form-label form-check-label"
-                    htmlFor="bindByEmail-SAML"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
-                </p>
+            <div className="row mb-5 ms-3">
+              <div className="form-check form-check-success">
+                <input
+                  id="bindByEmail-SAML"
+                  className="form-check-input"
+                  type="checkbox"
+                  checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
+                  onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                />
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="bindByEmail-SAML"
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
+                />
               </div>
+              <p className="form-text text-muted">
+                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+              </p>
             </div>
 
-            <h3 className="alert-anchor border-bottom">
+            <h3 className="alert-anchor border-bottom mb-4">
               Attribute-based Login Control
             </h3>
 

+ 3 - 3
apps/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -75,8 +75,8 @@ const SecurityManagementContents = () => {
 
       {/* XSS configuration link */}
       <div className="mb-5">
-        <h2 className="border-bottom">{t('security_settings.xss_prevent_setting')}</h2>
-        <div className="text-center">
+        <h2 className="border-bottom pb-2">{t('security_settings.xss_prevent_setting')}</h2>
+        <div className="mt-4">
           <Link
             href="/admin/markdown/#preventXSS"
             style={{ fontSize: 'large' }}
@@ -87,7 +87,7 @@ const SecurityManagementContents = () => {
       </div>
 
       <div className="auth-mechanism-configurations">
-        <h2 className="border-bottom">{t('security_settings.Authentication mechanism settings')}</h2>
+        <h2 className="border-bottom pb-2">{t('security_settings.Authentication mechanism settings')}</h2>
         <CustomNav
           activeTab={activeTab}
           navTabMapping={navTabMapping}

+ 12 - 12
apps/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -252,7 +252,7 @@ class SecuritySetting extends React.Component {
     return (
       <div key={`page-delete-permission-dropdown-${deletionType}`} className="row">
 
-        <div className="col-md-3 text-md-end">
+        <div className="col-md-4 text-md-end">
           {!isRecursiveDeletion(deletionType) && isTypeDeletion(deletionType) && (
             <strong>{t('security_settings.page_delete')}</strong>
           )}
@@ -261,7 +261,7 @@ class SecuritySetting extends React.Component {
           )}
         </div>
 
-        <div className="col-md-6">
+        <div className="col-md-8">
           {
             !isRecursiveDeletion(deletionType)
               ? (
@@ -301,7 +301,7 @@ class SecuritySetting extends React.Component {
                   </button>
                   <Collapse isOpen={expantDeleteOptionsState}>
                     <div className="pb-4">
-                      <p className="card custom-card">
+                      <p className="card custom-card bg-warning-sublte">
                         <span className="text-warning">
                           <span className="material-symbols-outlined">info</span>
                           {/* eslint-disable-next-line react/no-danger */}
@@ -412,12 +412,12 @@ class SecuritySetting extends React.Component {
           </table>
         </div>
 
-        <h4>{t('security_settings.page_access_rights')}</h4>
+        <h4 className="mb-3">{t('security_settings.page_access_rights')}</h4>
         <div className="row mb-4">
-          <div className="col-md-3 text-md-end py-2">
+          <div className="col-md-4 text-md-end py-2">
             <strong>{t('security_settings.Guest Users Access')}</strong>
           </div>
-          <div className="col-md-9">
+          <div className="col-md-8">
             <div className="dropdown">
               <button
                 className={`btn btn-outline-secondary dropdown-toggle text-end col-12
@@ -457,7 +457,7 @@ class SecuritySetting extends React.Component {
           </div>
         </div>
 
-        <h4>{t('security_settings.page_delete_rights')}</h4>
+        <h4 className="mb-3">{t('security_settings.page_delete_rights')}</h4>
         {/* Render PageDeletePermission */}
         {
           [
@@ -474,9 +474,9 @@ class SecuritySetting extends React.Component {
           ].map(arr => this.renderPageDeletePermission(arr[0], arr[1], arr[2], arr[3]))
         }
 
-        <h4>{t('security_settings.user_homepage_deletion.user_homepage_deletion')}</h4>
+        <h4 className="mb-3">{t('security_settings.user_homepage_deletion.user_homepage_deletion')}</h4>
         <div className="row mb-4">
-          <div className="col-6 offset-3">
+          <div className="col-md-10 offset-md-2">
             <div className="form-check form-switch form-check-success">
               <input
                 type="checkbox"
@@ -512,9 +512,9 @@ class SecuritySetting extends React.Component {
         <h4>{t('security_settings.session')}</h4>
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.max_age')}</label>
-          <div className="col-md-6">
+          <div className="col-md-8">
             <input
-              className="form-control col-md-3"
+              className="form-control col-md-4"
               type="text"
               defaultValue={adminGeneralSecurityContainer.state.sessionMaxAge || ''}
               onChange={(e) => {
@@ -524,7 +524,7 @@ class SecuritySetting extends React.Component {
             />
             {/* eslint-disable-next-line react/no-danger */}
             <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
-            <p className="card custom-card">
+            <p className="card custom-card bg-warning-subtle">
               <span className="text-warning">
                 <span className="material-symbols-outlined">info</span> {t('security_settings.max_age_caution')}
               </span>

+ 11 - 12
apps/app/src/components/Admin/Security/ShareLinkSetting.tsx

@@ -102,19 +102,9 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
 
   return (
     <>
-      <div className="mb-3">
-        <button
-          className="pull-right btn btn-danger"
-          disabled={shareLinks.length === 0}
-          type="button"
-          onClick={() => setIsDeleteConfirmModalShown(true)}
-        >
-          {t('security_settings.delete_all_share_links')}
-        </button>
-        <h2 className="alert-anchor border-bottom">{t('security_settings.share_link_management')}</h2>
-      </div>
+      <h2 className="alert-anchor border-bottom mb-4">{t('security_settings.share_link_management')}</h2>
       <h4>{t('security_settings.share_link_rights')}</h4>
-      <div className="row mb-5">
+      <div className="row mt-4 mb-5">
         <div className="col-6 offset-3">
           <div className="form-check form-switch form-check-success">
             <input
@@ -154,6 +144,15 @@ const ShareLinkSetting = (props: ShareLinkSettingProps) => {
         )
       }
 
+      <button
+        className="pull-right btn btn-danger mt-2"
+        disabled={shareLinks.length === 0}
+        type="button"
+        onClick={() => setIsDeleteConfirmModalShown(true)}
+      >
+        {t('security_settings.delete_all_share_links')}
+      </button>
+
       <DeleteAllShareLinksModal
         isOpen={isDeleteConfirmModalShown}
         onClose={() => setIsDeleteConfirmModalShown(false)}

+ 9 - 9
apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -74,18 +74,18 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
         )}
         {
           userGroup?.createdAt != null && (
-            <div className="row">
+            <div className="row mb-3">
               <p className="col-md-2 col-form-label">{t('Created')}</p>
-              <p className="col-md-4 my-auto">{dateFnsFormat(new Date(userGroup.createdAt), 'yyyy-MM-dd')}</p>
+              <p className="col-md-6 my-auto">{dateFnsFormat(new Date(userGroup.createdAt), 'yyyy-MM-dd')}</p>
             </div>
           )
         }
 
-        <div className="row">
+        <div className="row mb-3">
           <label htmlFor="name" className="col-md-2 col-form-label">
             {t('user_group_management.group_name')}
           </label>
-          <div className="col-md-4 my-auto">
+          <div className="col-md-6 my-auto">
             <input
               className="form-control"
               type="text"
@@ -99,20 +99,20 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
           </div>
         </div>
 
-        <div className="row">
+        <div className="row mb-3">
           <label htmlFor="description" className="col-md-2 col-form-label">
             {t('Description')}
           </label>
-          <div className="col-md-4">
+          <div className="col-md-6">
             <textarea className="form-control" name="description" value={currentDescription} onChange={onChangeDescriptionHandler} />
           </div>
         </div>
 
-        <div className="row">
+        <div className="row mb-3">
           <label htmlFor="parent" className="col-md-2 col-form-label">
             {t('user_group_management.parent_group')}
           </label>
-          <div className="dropdown col-md-4">
+          <div className="dropdown col-md-6">
             <button
               type="button"
               id="dropdownMenuButton"
@@ -154,7 +154,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
           </div>
         </div>
 
-        <div className="row">
+        <div className="row mb-5">
           <div className="offset-md-2 col-md-10">
             <button type="submit" className="btn btn-primary">
               {submitButtonLabel}

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

@@ -138,7 +138,7 @@ export const UserGroupTable: FC<Props> = ({
   }, [userGroupRelations, childUserGroups]);
 
   return (
-    <div data-testid="grw-user-group-table">
+    <div data-testid="grw-user-group-table" className="mb-5">
       <h3>{headerLabel}</h3>
 
       <table className="table table-bordered table-user-list">
@@ -179,7 +179,7 @@ export const UserGroupTable: FC<Props> = ({
                 <td>
                   <ul className="list-inline">
                     {users != null && users.map((user) => {
-                      return <li key={user._id} className="list-inline-item badge rounded-pill bg-warning text-dark">{user.username}</li>;
+                      return <li key={user._id} className="list-inline-item badge text-bg-warning">{user.username}</li>;
                     })}
                   </ul>
                 </td>
@@ -187,7 +187,7 @@ export const UserGroupTable: FC<Props> = ({
                   <ul className="list-inline">
                     {groupIdToChildGroupsMap[group._id] != null && groupIdToChildGroupsMap[group._id].map((group) => {
                       return (
-                        <li key={group._id} className="list-inline-item badge badge-success">
+                        <li key={group._id} className="list-inline-item badge text-bg-success">
                           {isAclEnabled
                             ? (
                               <Link href={`/admin/user-group-detail/${group._id}?isExternalGroup=${isExternalGroup}`}>{group.name}</Link>

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -17,7 +17,7 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
 
   return (
-    <table className="table table-bordered table-user-list">
+    <table className="table table-bordered table-user-list mb-5">
       <thead>
         <tr>
           <th style={{ width: '100px' }}>#</th>

+ 1 - 1
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -98,7 +98,7 @@ export const PagePathNav = (props: Props): JSX.Element => {
         { pageId != null && !isNotFound && (
           <div className="d-flex align-items-center ms-2">
             { isWipPage && (
-              <span className="badge rounded-pill text-bg-secondary ms-1 me-1">WIP</span>
+              <span className="badge text-bg-secondary ms-1 me-1">WIP</span>
             )}
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
               <span className="material-symbols-outlined">content_paste</span>

+ 18 - 1
apps/app/src/components/InstallerForm.tsx

@@ -10,7 +10,7 @@ import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
-
+import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 
 import styles from './InstallerForm.module.scss';
 
@@ -28,6 +28,8 @@ const InstallerForm = memo((): JSX.Element => {
   const [isLoading, setIsLoading] = useState(false);
   const [currentLocale, setCurrentLocale] = useState(isSupportedLang ? i18n.language : Lang.en_US);
 
+  const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
+
   const checkUserName = useCallback(async(event) => {
     const axios = require('axios').create({
       headers: {
@@ -70,6 +72,7 @@ const InstallerForm = memo((): JSX.Element => {
     };
 
     try {
+      setRegisterErrors([]);
       await apiv3Post('/installer', data);
       router.push('/');
     }
@@ -77,6 +80,7 @@ const InstallerForm = memo((): JSX.Element => {
       const err = errs[0];
       const code = err.code;
       setIsLoading(false);
+      setRegisterErrors(errs);
 
       if (code === 'failed_to_login_after_install') {
         toastError(t('installer.failed_to_login_after_install'));
@@ -103,6 +107,19 @@ const InstallerForm = memo((): JSX.Element => {
         </div>
       </div>
       <div className="row mt-2">
+
+        {
+          registerErrors != null && registerErrors.length > 0 && (
+            <p className="alert alert-danger text-center">
+              {registerErrors.map(err => (
+                <span>
+                  {t(err.message)}<br />
+                </span>
+              ))}
+            </p>
+          )
+        }
+
         <form role="form" id="register-form" className="ps-1" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
             <div className="input-group dropdown-with-icon">

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

@@ -34,9 +34,17 @@ $slack-work-space-name-card-border: #efc1f6;
   }
 
   .admin-setting-header {
+    margin-bottom: 20px;
     border-bottom: 1px solid transparent;
   }
 
+  .custom-card {
+    padding: 8px 16px;
+    ul {
+      margin-bottom: 0;
+    }
+  }
+
   .admin-security {
     .passport-logo {
       height: 32px;

+ 3 - 3
apps/app/src/components/Layout/AdminLayout.tsx

@@ -31,7 +31,7 @@ const AdminLayout = ({
     <RawLayout>
       <div className={`admin-page ${styles['admin-page']}`}>
 
-        <header className="py-0 container-fluid">
+        <header className="py-0 container">
           <h1 className="p-3 fs-2 d-flex align-items-center">
             <Link href="/" className="d-block mb-1 me-2">
               <GrowiLogo />
@@ -40,12 +40,12 @@ const AdminLayout = ({
           </h1>
         </header>
         <div className="main">
-          <div className="container-fluid">
+          <div className="container">
             <div className="row">
               <div className="col-lg-3">
                 <AdminNavigation />
               </div>
-              <div className="col-lg-9">
+              <div className="col-lg-9 mb-5">
                 {children}
               </div>
             </div>

+ 13 - 2
apps/app/src/components/Page/PageView.tsx

@@ -4,6 +4,7 @@ import React, {
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 
 import { useShouldExpandContent } from '~/client/services/layout';
@@ -40,6 +41,7 @@ const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod =>
 const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
   .then(mod => mod.UsersHomepageFooter), { ssr: false });
 const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
+const SlideRenderer = dynamic(() => import('./SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 
 type Props = {
@@ -74,6 +76,10 @@ export const PageView = (props: Props): JSX.Element => {
   const shouldExpandContent = useShouldExpandContent(page);
 
 
+  const markdown = page?.revision?.body;
+  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+
+
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
     // do nothing if hash is empty
@@ -90,6 +96,7 @@ export const PageView = (props: Props): JSX.Element => {
   }, [isCommentsLoaded]);
   // *******************************  end  *******************************
 
+
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
@@ -128,15 +135,19 @@ export const PageView = (props: Props): JSX.Element => {
       return <NotFoundPage path={pagePath} />;
     }
 
-    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const markdown = page.revision.body;
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
 
     return (
       <>
         <PageContentsUtilities />
 
         <div className="flex-expand-vert justify-content-between">
-          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+
+          { isSlide != null
+            ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+            : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+          }
 
           { !isIdenticalPathPage && !isNotFound && (
             <div id="comments-container" ref={commentsContainerRef}>

+ 3 - 1
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
-import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
+import type { FallbackProps } from 'react-error-boundary';
+import { ErrorBoundary } from 'react-error-boundary';
 import ReactMarkdown from 'react-markdown';
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
@@ -8,6 +9,7 @@ import loggerFactory from '~/utils/logger';
 
 import 'katex/dist/katex.min.css';
 
+
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 type Props = {

+ 26 - 0
apps/app/src/components/Page/SlideRenderer.tsx

@@ -0,0 +1,26 @@
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+
+import { usePresentationViewOptions } from '~/stores/renderer';
+
+import { Slides } from '../Presentation/Slides';
+
+type SlideRendererProps = {
+  markdown: string,
+  marp?: boolean,
+};
+
+export const SlideRenderer = (props: SlideRendererProps): JSX.Element => {
+
+  const { markdown, marp = false } = props;
+
+  const { data: rendererOptions } = usePresentationViewOptions();
+
+  return (
+    <Slides
+      hasMarpFlag={marp}
+      options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
+    >
+      {markdown}
+    </Slides>
+  );
+};

+ 2 - 2
apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx

@@ -52,8 +52,8 @@ const ShareLinkTr = (props: ShareLinkTrProps): JSX.Element => {
       <td style={{ verticalAlign: 'middle' }}>
         {shareLink.expiredAt && <span>{dateFnsFormat(new Date(shareLink.expiredAt), 'yyyy-MM-dd HH:mm')}</span>}
       </td>
-      <td style={{ maxWidth: '0', textAlign: 'center' }}>
-        <button className="btn btn-outline-warning" type="button" onClick={onDelete}>
+      <td style={{ maxWidth: '50', textAlign: 'center' }}>
+        <button className="btn btn-outline-danger" type="button" onClick={onDelete}>
           <span className="material-symbols-outlined">delete</span>{t('Delete')}
         </button>
       </td>

+ 15 - 5
apps/app/src/components/PageEditor/Preview.tsx

@@ -1,10 +1,12 @@
 import type { CSSProperties } from 'react';
-import React from 'react';
+
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { useIsEnabledMarp } from '~/stores/context';
 
 import RevisionRenderer from '../Page/RevisionRenderer';
-
+import { SlideRenderer } from '../Page/SlideRenderer';
 
 import styles from './Preview.module.scss';
 
@@ -28,17 +30,25 @@ const Preview = (props: Props): JSX.Element => {
     expandContentWidth,
   } = props;
 
+  const { data: isEnabledMarp } = useIsEnabledMarp();
+  const isSlide = useSlidesByFrontmatter(markdown, isEnabledMarp);
+
   const fluidLayoutClass = expandContentWidth ? 'fluid-layout' : '';
 
+
   return (
     <div
       data-testid="page-editor-preview-body"
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       style={style}
     >
-      { markdown != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
-      ) }
+      { markdown != null
+        && (
+          isSlide != null
+            ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+            : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
+        )
+      }
     </div>
   );
 

+ 7 - 4
apps/app/src/components/PagePresentationModal.tsx

@@ -1,6 +1,7 @@
 import React, { useCallback } from 'react';
 
-import type { PresentationProps } from '@growi/presentation';
+import type { PresentationProps } from '@growi/presentation/dist/client';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
@@ -39,6 +40,10 @@ const PagePresentationModal = (): JSX.Element => {
 
   const { data: isEnabledMarp } = useIsEnabledMarp();
 
+  const markdown = currentPage?.revision?.body;
+
+  const isSlide = useSlidesByFrontmatter(markdown, isEnabledMarp);
+
   const toggleFullscreenHandler = useCallback(() => {
     if (fullscreen.active) {
       fullscreen.exit();
@@ -61,8 +66,6 @@ const PagePresentationModal = (): JSX.Element => {
     return <></>;
   }
 
-  const markdown = currentPage?.revision?.body;
-
   return (
     <Modal
       isOpen={isOpen}
@@ -92,7 +95,7 @@ const PagePresentationModal = (): JSX.Element => {
               },
               isDarkMode,
             }}
-            isEnabledMarp={isEnabledMarp}
+            marp={isSlide?.marp}
           >
             {markdown}
           </Presentation>

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

@@ -1,4 +1,4 @@
-import { Presentation as PresentationSubstance, type PresentationProps } from '@growi/presentation';
+import { Presentation as PresentationSubstance, type PresentationProps } from '@growi/presentation/dist/client';
 
 import '@growi/presentation/dist/style.css';
 

+ 1 - 1
apps/app/src/components/Presentation/Slides.tsx

@@ -1,4 +1,4 @@
-import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation';
+import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation/dist/client';
 
 import '@growi/presentation/dist/style.css';
 

+ 0 - 33
apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx

@@ -1,33 +0,0 @@
-import React from 'react';
-
-import dynamic from 'next/dynamic';
-import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
-
-import { usePresentationViewOptions } from '~/stores/slide-viewer-renderer';
-
-
-const Slides = dynamic(() => import('../Presentation/Slides').then(mod => mod.Slides), { ssr: false });
-
-type SlideViewerProps = {
-  marp: string | undefined,
-  children: string,
-}
-
-export const SlideViewer: React.FC<SlideViewerProps> = React.memo((props: SlideViewerProps) => {
-  const {
-    marp, children,
-  } = props;
-
-  const { data: rendererOptions } = usePresentationViewOptions();
-
-  return (
-    <Slides
-      hasMarpFlag={marp != null}
-      options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
-    >
-      {children}
-    </Slides>
-  );
-});
-
-SlideViewer.displayName = 'SlideViewer';

+ 18 - 6
apps/app/src/components/ShareLinkPageView.tsx

@@ -1,12 +1,14 @@
 import React, { useMemo } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
+import { useIsEnabledMarp } from '~/stores/context';
 import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
@@ -15,6 +17,7 @@ import { PagePathNavSticky } from './Common/PagePathNav';
 import { PageViewLayout } from './Common/PageViewLayout';
 import RevisionRenderer from './Page/RevisionRenderer';
 import ShareLinkAlert from './Page/ShareLinkAlert';
+import { PageContentFooter } from './PageContentFooter';
 import type { PageSideContentsProps } from './PageSideContents';
 
 
@@ -23,7 +26,7 @@ const logger = loggerFactory('growi:Page');
 
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('./PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const ForbiddenPage = dynamic(() => import('./ForbiddenPage'), { ssr: false });
-
+const SlideRenderer = dynamic(() => import('./Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 type Props = {
   pagePath: string,
@@ -47,6 +50,10 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
   const shouldExpandContent = useShouldExpandContent(page);
 
+  const markdown = page?.revision?.body;
+
+  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
 
   const specialContents = useMemo(() => {
@@ -66,6 +73,12 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     : null;
 
 
+  const footerContents = !isNotFound
+    ? (
+      <PageContentFooter page={page} />
+    )
+    : null;
+
   const Contents = () => {
     if (isNotFound || page.revision == null) {
       return <></>;
@@ -85,11 +98,9 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const markdown = page.revision.body;
 
-    return (
-      <>
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      </>
-    );
+    return isSlide != null
+      ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+      : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />;
   };
 
   return (
@@ -97,6 +108,7 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
       headerContents={headerContents}
       sideContents={sideContents}
       expandContentWidth={shouldExpandContent}
+      footerContents={footerContents}
     >
       { specialContents }
       { specialContents == null && (

+ 6 - 6
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -184,13 +184,13 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
         CustomTreeItem={PageTreeItem}
       />
 
-      {/* {!isGuestUser && !isReadOnlyUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && ( */}
-      <div className="grw-pagetree-footer border-top mt-4 py-2 w-100">
-        <div className="private-legacy-pages-link px-3 py-2">
-          <PrivateLegacyPagesLink />
+      {!isGuestUser && !isReadOnlyUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
+        <div className="grw-pagetree-footer border-top mt-4 py-2 w-100">
+          <div className="private-legacy-pages-link px-3 py-2">
+            <PrivateLegacyPagesLink />
+          </div>
         </div>
-      </div>
-      {/* )} */}
+      )}
     </div>
   );
 });

+ 1 - 1
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -147,7 +147,7 @@ export const ExternalGroupManagement: FC = () => {
 
   return (
     <>
-      <h2 className="border-bottom">{t('external_user_group.management')}</h2>
+      <h2 className="border-bottom mb-4">{t('external_user_group.management')}</h2>
       <UserGroupTable
         headerLabel={t('admin:user_group_management.group_list')}
         userGroups={externalUserGroups}

+ 22 - 23
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/KeycloakGroupSyncSettingsForm.tsx

@@ -1,13 +1,12 @@
-import {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxKeycloakGroupSyncSettings } from '~/features/external-user-group/client/stores/external-user-group';
-import { KeycloakGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
+import type { KeycloakGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
 
 export const KeycloakGroupSyncSettingsForm: FC = () => {
   const { t } = useTranslation('admin');
@@ -49,11 +48,11 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
         <div className="row form-group">
           <label
             htmlFor="keycloakHost"
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {t('external_user_group.keycloak.host')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               type="text"
@@ -68,10 +67,10 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupRealm" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="keycloakGroupRealm" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.keycloak.group_realm')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -89,10 +88,10 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupSyncClientRealm" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="keycloakGroupSyncClientRealm" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.keycloak.group_sync_client_realm')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -110,10 +109,10 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupSyncClientID" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="keycloakGroupSyncClientID" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.keycloak.group_sync_client_id')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -131,10 +130,10 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupSyncClientSecret" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="keycloakGroupSyncClientSecret" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.keycloak.group_sync_client_secret')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -153,15 +152,15 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
         </div>
         <div className="row form-group">
           <label
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {/* {t('external_user_group.auto_generate_user_on_sync')} */}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <div className="custom-control custom-checkbox custom-checkbox-info">
               <input
                 type="checkbox"
-                className="custom-control-input"
+                className="custom-control-input me-2"
                 name="autoGenerateUserOnKeycloakGroupSync"
                 id="autoGenerateUserOnKeycloakGroupSync"
                 checked={formValues.autoGenerateUserOnKeycloakGroupSync}
@@ -178,15 +177,15 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
         </div>
         <div className="row form-group">
           <label
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {/* {t('external_user_group.keycloak.preserve_deleted_keycloak_groups')} */}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <div className="custom-control custom-checkbox custom-checkbox-info">
               <input
                 type="checkbox"
-                className="custom-control-input"
+                className="custom-control-input me-2"
                 name="preserveDeletedKeycloakGroups"
                 id="preserveDeletedKeycloakGroups"
                 checked={formValues.preserveDeletedKeycloakGroups}
@@ -201,14 +200,14 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
             </div>
           </div>
         </div>
-        <div className="px-5">
+        <div className="mt-5 mb-4">
           <h4 className="border-bottom mb-3">Attribute Mapping ({t('optional')})</h4>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupDescriptionAttribute" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="keycloakGroupDescriptionAttribute" className="text-left text-md-end col-md-3 col-form-label">
             {t('Description')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               type="text"

+ 23 - 23
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/LdapGroupSyncSettingsForm.tsx

@@ -1,13 +1,13 @@
-import {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxLdapGroupSyncSettings } from '~/features/external-user-group/client/stores/external-user-group';
-import { LdapGroupMembershipAttributeType, LdapGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
+import type { LdapGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
+import { LdapGroupMembershipAttributeType } from '~/features/external-user-group/interfaces/external-user-group';
 
 export const LdapGroupSyncSettingsForm: FC = () => {
   const { t } = useTranslation('admin');
@@ -49,11 +49,11 @@ export const LdapGroupSyncSettingsForm: FC = () => {
         <div className="row form-group">
           <label
             htmlFor="ldapGroupSearchBase"
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {t('external_user_group.ldap.group_search_base_DN')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               type="text"
@@ -68,10 +68,10 @@ export const LdapGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="ldapGroupMembershipAttribute" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="ldapGroupMembershipAttribute" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.ldap.membership_attribute')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -90,10 +90,10 @@ export const LdapGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="ldapGroupMembershipAttributeType" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="ldapGroupMembershipAttributeType" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.ldap.membership_attribute_type')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <select
               className="form-control"
               required
@@ -117,10 +117,10 @@ export const LdapGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="ldapGroupChildGroupAttribute" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="ldapGroupChildGroupAttribute" className="text-left text-md-end col-md-3 col-form-label">
             {t('external_user_group.ldap.child_group_attribute')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               required
@@ -140,15 +140,15 @@ export const LdapGroupSyncSettingsForm: FC = () => {
         </div>
         <div className="row form-group">
           <label
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {/* {t('external_user_group.auto_generate_user_on_sync')} */}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <div className="custom-control custom-checkbox custom-checkbox-info">
               <input
                 type="checkbox"
-                className="custom-control-input"
+                className="custom-control-input me-2"
                 name="autoGenerateUserOnLdapGroupSync"
                 id="autoGenerateUserOnLdapGroupSync"
                 checked={formValues.autoGenerateUserOnLdapGroupSync}
@@ -165,15 +165,15 @@ export const LdapGroupSyncSettingsForm: FC = () => {
         </div>
         <div className="row form-group">
           <label
-            className="text-left text-md-right col-md-3 col-form-label"
+            className="text-left text-md-end col-md-3 col-form-label"
           >
             {/* {t('external_user_group.ldap.preserve_deleted_ldap_groups')} */}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <div className="custom-control custom-checkbox custom-checkbox-info">
               <input
                 type="checkbox"
-                className="custom-control-input"
+                className="custom-control-input me-2"
                 name="preserveDeletedLdapGroups"
                 id="preserveDeletedLdapGroups"
                 checked={formValues.preserveDeletedLdapGroups}
@@ -188,12 +188,12 @@ export const LdapGroupSyncSettingsForm: FC = () => {
             </div>
           </div>
         </div>
-        <div className="px-5">
+        <div className="mt-5 mb-4">
           <h4 className="border-bottom mb-3">Attribute Mapping ({t('optional')})</h4>
         </div>
         <div className="row form-group">
-          <label htmlFor="ldapGroupNameAttribute" className="text-left text-md-right col-md-3 col-form-label">{t('Name')}</label>
-          <div className="col-md-6">
+          <label htmlFor="ldapGroupNameAttribute" className="text-left text-md-end col-md-3 col-form-label">{t('Name')}</label>
+          <div className="col-md-9">
             <input
               className="form-control"
               type="text"
@@ -211,10 +211,10 @@ export const LdapGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="ldapGroupDescriptionAttribute" className="text-left text-md-right col-md-3 col-form-label">
+          <label htmlFor="ldapGroupDescriptionAttribute" className="text-left text-md-end col-md-3 col-form-label">
             {t('Description')}
           </label>
-          <div className="col-md-6">
+          <div className="col-md-9">
             <input
               className="form-control"
               type="text"

+ 3 - 2
apps/app/src/features/questionnaire/client/stores/questionnaire.tsx

@@ -1,8 +1,9 @@
-import useSWR, { SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+import useSWR from 'swr';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 
-import { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
+import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
 
 export const useSWRxQuestionnaireOrders = (): SWRResponse<IQuestionnaireOrderHasId[], Error> => {
   return useSWR(

+ 10 - 5
apps/app/src/server/models/activity.ts

@@ -1,13 +1,17 @@
 import type { Ref, IPage } from '@growi/core';
-import {
-  Types, Document, Model, Schema, SortOrder,
+import type {
+  Types, Document, Model, SortOrder,
 } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
+import type {
+  IActivity, ISnapshot, SupportedActionType, SupportedTargetModelType, SupportedEventModelType,
+} from '~/interfaces/activity';
 import {
-  IActivity, ISnapshot, AllSupportedActions, SupportedActionType,
-  AllSupportedTargetModels, SupportedTargetModelType,
-  AllSupportedEventModels, SupportedEventModelType,
+  AllSupportedActions,
+  AllSupportedTargetModels,
+  AllSupportedEventModels,
 } from '~/interfaces/activity';
 
 import loggerFactory from '../../utils/logger';
@@ -83,6 +87,7 @@ const activitySchema = new Schema<ActivityDocument, ActivityModel>({
     updatedAt: false,
   },
 });
+// activitySchema.index({ createdAt: 1 }); // Do not create index here because it is created by ActivityService as TTL index
 activitySchema.index({ target: 1, action: 1 });
 activitySchema.index({
   user: 1, target: 1, action: 1, createdAt: 1,

+ 6 - 4
apps/app/src/server/models/in-app-notification.ts

@@ -1,6 +1,5 @@
-import {
-  Types, Document, Schema, Model,
-} from 'mongoose';
+import type { Types, Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import { AllSupportedTargetModels, AllSupportedActions } from '~/interfaces/activity';
@@ -8,7 +7,7 @@ import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-import { ActivityDocument } from './activity';
+import type { ActivityDocument } from './activity';
 
 
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
@@ -79,6 +78,9 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
 }, {
   timestamps: { createdAt: true, updatedAt: false },
 });
+// indexes
+inAppNotificationSchema.index({ createdAt: 1 });
+// apply plugins
 inAppNotificationSchema.plugin(mongoosePaginate);
 
 const transform = (doc, ret) => {

+ 3 - 0
apps/app/src/server/models/page.ts

@@ -153,6 +153,9 @@ const schema = new Schema<PageDocument, PageModel>({
   toJSON: { getters: true },
   toObject: { getters: true },
 });
+// indexes
+schema.index({ createdAt: 1 });
+schema.index({ updatedAt: 1 });
 // apply plugins
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);

+ 2 - 2
apps/app/src/server/models/user.js

@@ -49,7 +49,7 @@ module.exports = function(crowi) {
     isGravatarEnabled: { type: Boolean, default: false },
     isEmailPublished: { type: Boolean, default: true },
     googleId: String,
-    name: { type: String },
+    name: { type: String, index: true },
     username: { type: String, required: true, unique: true },
     email: { type: String, unique: true, sparse: true },
     slackMemberId: { type: String, unique: true, sparse: true },
@@ -69,7 +69,7 @@ module.exports = function(crowi) {
     status: {
       type: Number, required: true, default: STATUS_ACTIVE, index: true,
     },
-    lastLoginAt: { type: Date },
+    lastLoginAt: { type: Date, index: true },
     admin: { type: Boolean, default: 0, index: true },
     readOnly: { type: Boolean, default: 0 },
     isInvitationEmailSended: { type: Boolean, default: false },

+ 12 - 6
apps/app/src/server/routes/apiv3/installer.ts

@@ -1,16 +1,16 @@
 import { ErrorV3 } from '@growi/core/dist/models';
-import express, { Request, Router } from 'express';
+import type { Request, Router } from 'express';
+import express from 'express';
 
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
-import Crowi from '../../crowi';
+import type Crowi from '../../crowi';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
-import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-import { registerRules } from '../../middlewares/register-form-validator';
+import { registerRules, registerValidation } from '../../middlewares/register-form-validator';
 import { InstallerService, FailedToCreateAdminUserError } from '../../service/installer';
 
-import { ApiV3Response } from './interfaces/apiv3-response';
+import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -27,11 +27,17 @@ module.exports = (crowi: Crowi): Router => {
   const router = express.Router();
 
   // eslint-disable-next-line max-len
-  router.post('/', registerRules(), apiV3FormValidator, addActivity, async(req: FormRequest, res: ApiV3Response) => {
+  router.post('/', registerRules(), registerValidation, addActivity, async(req: FormRequest, res: ApiV3Response) => {
     const appService = crowi.appService;
     if (appService == null) {
       return res.apiv3Err(new ErrorV3('GROWI cannot be installed due to an internal error', 'app_service_not_setup'), 500);
     }
+
+    if (!req.form.isValid) {
+      const errors = req.form.errors;
+      return res.apiv3Err(errors, 400);
+    }
+
     const registerForm = req.body.registerForm || {};
 
     const name = registerForm.name;

+ 6 - 0
apps/app/src/server/service/config-loader.ts

@@ -471,6 +471,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.NUMBER,
     default: 120,
   },
+  S3_OBJECT_ACL: {
+    ns:      'crowi',
+    key:     'aws:s3ObjectCannedACL',
+    type:    ValueType.STRING,
+    default: 'public-read',
+  },
   GCS_API_KEY_JSON_PATH: {
     ns:      'crowi',
     key:     'gcs:apiKeyJsonPath',

+ 28 - 3
apps/app/src/server/service/file-uploader/aws.ts

@@ -48,6 +48,30 @@ const isFileExists = async(s3: S3Client, params: HeadObjectCommandInput) => {
   return true;
 };
 
+const ObjectCannedACLs = [
+  ObjectCannedACL.authenticated_read,
+  ObjectCannedACL.aws_exec_read,
+  ObjectCannedACL.bucket_owner_full_control,
+  ObjectCannedACL.bucket_owner_read,
+  ObjectCannedACL.private,
+  ObjectCannedACL.public_read,
+  ObjectCannedACL.public_read_write,
+];
+const isValidObjectCannedACL = (acl: string | null): acl is ObjectCannedACL => {
+  return ObjectCannedACLs.includes(acl as ObjectCannedACL);
+};
+/**
+ * @see: https://dev.growi.org/5d091f611fe336003eec5bfdz
+ * @returns ObjectCannedACL
+ */
+const getS3PutObjectCannedAcl = (): ObjectCannedACL | undefined => {
+  const s3ObjectCannedACL = configManager.getConfig('crowi', 'aws:s3ObjectCannedACL');
+  if (isValidObjectCannedACL(s3ObjectCannedACL)) {
+    return s3ObjectCannedACL;
+  }
+  return undefined;
+};
+
 const getS3Bucket = (): string | undefined => {
   return configManager.getConfig('crowi', 'aws:s3Bucket') ?? undefined; // return undefined when getConfig() returns null
 };
@@ -212,7 +236,8 @@ module.exports = (crowi) => {
         configManager.getConfig('crowi', 'aws:s3Region') != null
           || configManager.getConfig('crowi', 'aws:s3CustomEndpoint') != null
       )
-      && configManager.getConfig('crowi', 'aws:s3Bucket') != null;
+      && configManager.getConfig('crowi', 'aws:s3Bucket') != null
+      && configManager.getConfig('crowi', 'aws:s3BucketAclsDisable') != null;
   };
 
   (lib as any).deleteFile = async function(attachment) {
@@ -274,7 +299,7 @@ module.exports = (crowi) => {
       Bucket: getS3Bucket(),
       Key: filePath,
       Body: fileStream,
-      ACL: ObjectCannedACL.public_read,
+      ACL: getS3PutObjectCannedAcl(),
       // put type and the file name for reference information when uploading
       ContentType: contentHeaders.contentType?.value.toString(),
       ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
@@ -289,7 +314,7 @@ module.exports = (crowi) => {
       ContentType: contentType,
       Key: filePath,
       Body: data,
-      ACL: ObjectCannedACL.public_read,
+      ACL: getS3PutObjectCannedAcl(),
     }));
   };
 

+ 0 - 27
apps/app/src/stores/slide-viewer-renderer.tsx

@@ -1,27 +0,0 @@
-import useSWR, { type SWRResponse } from 'swr';
-
-import type { RendererOptions } from '~/interfaces/renderer-options';
-import { useRendererConfig } from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
-
-
-export const usePresentationViewOptions = (): SWRResponse<RendererOptions, Error> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: rendererConfig } = useRendererConfig();
-
-  const isAllDataValid = currentPagePath != null && rendererConfig != null;
-
-  return useSWR(
-    isAllDataValid
-      ? ['presentationViewOptions', currentPagePath, rendererConfig]
-      : null,
-    async([, currentPagePath, rendererConfig]) => {
-      const { generatePresentationViewOptions } = await import('~/client/services/renderer/slide-viewer-renderer');
-      return generatePresentationViewOptions(rendererConfig, currentPagePath);
-    },
-    {
-      revalidateOnFocus: false,
-      revalidateOnReconnect: false,
-    },
-  );
-};

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.0.4-slackbot-proxy.0",
+  "version": "7.0.5-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/editor/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/editor",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "license": "MIT",
   "type": "module",
   "module": "dist/index.js",

+ 15 - 3
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": [
@@ -13,6 +13,17 @@
     "dist"
   ],
   "type": "module",
+  "exports": {
+    "./dist/client": {
+      "import": "./dist/client/index.js"
+    },
+    "./dist/services": {
+      "import": "./dist/services/index.js"
+    },
+    "./dist/style.css": {
+      "import": "./dist/style.css"
+    }
+  },
   "scripts": {
     "build": "vite build",
     "clean": "shx rm -rf dist",
@@ -28,7 +39,8 @@
     "@growi/core": "link:../core"
   },
   "devDependencies": {
-    "@marp-team/marp-core": "^3.6.0",
+    "@marp-team/marp-core": "^3.9.0",
+    "@marp-team/marpit": "^2.6.1",
     "@types/reveal.js": "^4.4.1",
     "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^4.1.0",
@@ -38,6 +50,7 @@
     "mdast-util-to-markdown": "^1.3.0",
     "react-markdown": "^8.0.7",
     "remark-frontmatter": "^4.0.1",
+    "remark-parse": "^10.0.0",
     "remark-stringify": "^10.0.0",
     "reveal.js": "^4.4.0",
     "unified": "^10.1.2",
@@ -45,7 +58,6 @@
     "unist-util-visit": "^4.0.0"
   },
   "peerDependencies": {
-    "@marp-team/marpit": "*",
     "next": "^14",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"

+ 0 - 0
packages/presentation/src/components/GrowiSlides.tsx → packages/presentation/src/client/components/GrowiSlides.tsx


+ 0 - 0
packages/presentation/src/components/MarpSlides.tsx → packages/presentation/src/client/components/MarpSlides.tsx


+ 0 - 0
packages/presentation/src/components/Presentation.global.scss → packages/presentation/src/client/components/Presentation.global.scss


+ 0 - 0
packages/presentation/src/components/Presentation.module.scss → packages/presentation/src/client/components/Presentation.module.scss


+ 14 - 18
packages/presentation/src/components/Presentation.tsx → packages/presentation/src/client/components/Presentation.tsx

@@ -1,9 +1,8 @@
-import React, { useEffect } from 'react';
+import { useEffect } from 'react';
 
 import Reveal from 'reveal.js';
 
 import type { PresentationOptions } from '../consts';
-import { parseSlideFrontmatterInMarkdown } from '../services/parse-slide-frontmatter';
 
 import { Slides } from './Slides';
 
@@ -34,37 +33,34 @@ const removeAllHiddenElements = () => {
 
 export type PresentationProps = {
   options: PresentationOptions,
-  isEnabledMarp: boolean,
+  marp?: boolean,
   children?: string,
 }
 
 export const Presentation = (props: PresentationProps): JSX.Element => {
-  const { options, isEnabledMarp, children } = props;
+  const { options, marp, children } = props;
   const { revealOptions } = options;
 
-  const [marp] = parseSlideFrontmatterInMarkdown(children);
-  const hasMarpFlag = isEnabledMarp && marp;
-
   useEffect(() => {
-    let deck: Reveal.Api;
-    if (children != null) {
-      deck = new Reveal({ ...baseRevealOptions, ...revealOptions });
-      deck.initialize()
-        .then(() => deck.slide(0)); // navigate to the first slide
-
-      deck.on('ready', removeAllHiddenElements);
-      deck.on('slidechanged', removeAllHiddenElements);
+    if (children == null) {
+      return;
     }
+    const deck = new Reveal({ ...baseRevealOptions, ...revealOptions });
+    deck.initialize()
+      .then(() => deck.slide(0)); // navigate to the first slide
+
+    deck.on('ready', removeAllHiddenElements);
+    deck.on('slidechanged', removeAllHiddenElements);
 
     return function cleanup() {
-      deck?.off('ready', removeAllHiddenElements);
-      deck?.off('slidechanged', removeAllHiddenElements);
+      deck.off('ready', removeAllHiddenElements);
+      deck.off('slidechanged', removeAllHiddenElements);
     };
   }, [children, revealOptions]);
 
   return (
     <div className={`grw-presentation ${styles['grw-presentation']} reveal`}>
-      <Slides options={options} hasMarpFlag={hasMarpFlag} presentation>{children}</Slides>
+      <Slides options={options} hasMarpFlag={marp} presentation>{children}</Slides>
     </div>
   );
 };

+ 0 - 0
packages/presentation/src/components/RichSlideSection.tsx → packages/presentation/src/client/components/RichSlideSection.tsx


+ 0 - 0
packages/presentation/src/components/Slides.module.scss → packages/presentation/src/client/components/Slides.module.scss


+ 0 - 0
packages/presentation/src/components/Slides.tsx → packages/presentation/src/client/components/Slides.tsx


+ 0 - 0
packages/presentation/src/consts/index.ts → packages/presentation/src/client/consts/index.ts


+ 0 - 1
packages/presentation/src/index.ts → packages/presentation/src/client/index.ts

@@ -1,3 +1,2 @@
 export * from './components/Presentation';
 export * from './components/Slides';
-export * from './services/renderer/slides';

+ 0 - 0
packages/presentation/src/services/growi-marpit.ts → packages/presentation/src/client/services/growi-marpit.ts


+ 0 - 0
packages/presentation/src/services/renderer/extract-sections.ts → packages/presentation/src/client/services/renderer/extract-sections.ts


+ 1 - 0
packages/presentation/src/services/index.ts

@@ -0,0 +1 @@
+export * from './use-slides-by-frontmatter';

+ 0 - 43
packages/presentation/src/services/parse-slide-frontmatter.ts

@@ -1,43 +0,0 @@
-import remarkFrontmatter from 'remark-frontmatter';
-import remarkParse from 'remark-parse';
-import remarkStringify from 'remark-stringify';
-import { unified } from 'unified';
-
-
-export const parseSlideFrontmatter = (frontmatter: string): [boolean, boolean] => {
-
-  let marp = false;
-  let slide = false;
-
-  const lines = frontmatter.split('\n');
-  lines.forEach((line) => {
-    const [key, value] = line.split(':').map(part => part.trim());
-    if (key === 'marp' && value === 'true') {
-      marp = true;
-    }
-    if (key === 'slide' && value === 'true') {
-      slide = true;
-    }
-  });
-
-  return [marp, slide];
-};
-
-export const parseSlideFrontmatterInMarkdown = (markdown?: string): [boolean, boolean] => {
-
-  let marp = false;
-  let slide = false;
-
-  unified()
-    .use(remarkParse)
-    .use(remarkStringify)
-    .use(remarkFrontmatter, ['yaml'])
-    .use(() => ((obj) => {
-      if (obj.children[0]?.type === 'yaml') {
-        [marp, slide] = parseSlideFrontmatter(obj.children[0]?.value as string);
-      }
-    }))
-    .process(markdown as string);
-
-  return [marp, slide];
-};

+ 0 - 89
packages/presentation/src/services/renderer/slides.ts

@@ -1,89 +0,0 @@
-import type { Schema as SanitizeOption } from 'hast-util-sanitize';
-import type { Root } from 'mdast';
-import { frontmatterToMarkdown } from 'mdast-util-frontmatter';
-import { gfmToMarkdown } from 'mdast-util-gfm';
-import { toMarkdown } from 'mdast-util-to-markdown';
-import type { Plugin } from 'unified';
-import type { Node } from 'unist';
-import { visit } from 'unist-util-visit';
-
-import { parseSlideFrontmatter } from '../parse-slide-frontmatter';
-
-const SUPPORTED_ATTRIBUTES = ['children', 'marp'];
-
-const nodeToMakrdown = (node: Node) => {
-  return toMarkdown(node as Root, {
-    extensions: [
-      frontmatterToMarkdown(['yaml']),
-      gfmToMarkdown(),
-    ],
-  });
-};
-
-// Allow node tree to be converted to markdown
-const removeCustomType = (tree: Node) => {
-  // Try toMarkdown() on all Node.
-  visit(tree, (node) => {
-    const tmp = node?.children;
-    node.children = [];
-    try {
-      nodeToMakrdown(node);
-    }
-    catch (err) {
-      // if some Node cannot convert to markdown, change to a convertible type
-      node.type = 'text';
-      node.value = '';
-    }
-    finally {
-      node.children = tmp;
-    }
-  });
-};
-
-const rewriteNode = (tree: Node, node: Node, isEnabledMarp: boolean) => {
-
-  const [marp, slide] = parseSlideFrontmatter(node.value as string);
-
-  if ((marp && isEnabledMarp) || slide) {
-
-    removeCustomType(tree);
-
-    const markdown = nodeToMakrdown(tree);
-
-    const newNode: Node = {
-      type: 'root',
-      data: {},
-      position: tree.position,
-      children: tree.children,
-    };
-
-    const data = newNode.data ?? (newNode.data = {});
-    tree.children = [newNode];
-    data.hName = 'slide';
-    data.hProperties = {
-      marp: (marp && isEnabledMarp) ? '' : undefined,
-      children: markdown,
-    };
-  }
-};
-
-type SlidePluginParams = {
-  isEnabledMarp: boolean,
-}
-
-export const remarkPlugin: Plugin<[SlidePluginParams]> = (options) => {
-  return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'yaml' && node.value != null) {
-        rewriteNode(tree, node, options.isEnabledMarp);
-      }
-    });
-  };
-};
-
-export const sanitizeOption: SanitizeOption = {
-  tagNames: ['slide'],
-  attributes: {
-    slide: SUPPORTED_ATTRIBUTES,
-  },
-};

+ 97 - 0
packages/presentation/src/services/use-slides-by-frontmatter.ts

@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react';
+
+import type { Processor } from 'unified';
+
+type ParseResult = {
+  marp: boolean | undefined,
+  slide: boolean | undefined,
+}
+
+const parseSlideFrontmatter = (frontmatter: string): ParseResult => {
+
+  let marp;
+  let slide;
+
+  const lines = frontmatter.split('\n');
+  lines.forEach((line) => {
+    const [key, value] = line.split(':').map(part => part.trim());
+    if (key === 'marp' && value === 'true') {
+      marp = true;
+    }
+    if (key === 'slide' && value === 'true') {
+      slide = true;
+    }
+  });
+
+  return { marp, slide };
+};
+
+
+type ProcessorOpts = {
+  onParsed?: (result: ParseResult) => void,
+  onSkipped?: () => void,
+};
+
+const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
+
+  const remarkFrontmatter = (await import('remark-frontmatter')).default;
+  const remarkParse = (await import('remark-parse')).default;
+  const remarkStringify = (await import('remark-stringify')).default;
+  const unified = (await import('unified')).unified;
+
+  return unified()
+    .use(remarkParse)
+    .use(remarkStringify)
+    .use(remarkFrontmatter, ['yaml'])
+    .use(() => ((obj) => {
+      if (obj.children[0]?.type === 'yaml') {
+        const result = parseSlideFrontmatter(obj.children[0]?.value);
+        opts?.onParsed?.(result);
+      }
+      else {
+        opts?.onSkipped?.();
+      }
+    }));
+};
+
+export type UseSlide = {
+  marp?: boolean,
+}
+
+/**
+ * Frontmatter parser for slide
+ * @param markdown Markdwon document
+ * @returns An UseSlide instance. If the markdown does not contain neither "marp" or "slide" attribute in frontmatter, it returns undefined.
+ */
+export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolean): UseSlide | undefined => {
+
+  const [processor, setProcessor] = useState<Processor|undefined>();
+  const [parseResult, setParseResult] = useState<UseSlide|undefined>();
+
+  useEffect(() => {
+    if (processor != null) {
+      return;
+    }
+
+    (async() => {
+      const p = await generateFrontmatterProcessor({
+        onParsed: result => setParseResult(result.marp || result.slide ? result : undefined),
+        onSkipped: () => setParseResult(undefined),
+      });
+      setProcessor(p);
+    })();
+  }, [processor]);
+
+  useEffect(() => {
+    if (markdown == null || processor == null) {
+      return;
+    }
+
+    processor.process(markdown);
+  }, [markdown, processor]);
+
+  return parseResult != null
+    ? { marp: isEnabledMarp && parseResult?.marp }
+    : undefined;
+
+};

+ 12 - 1
packages/presentation/vite.config.ts

@@ -1,4 +1,7 @@
+import path from 'path';
+
 import react from '@vitejs/plugin-react';
+import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';
 import { defineConfig } from 'vite';
 import dts from 'vite-plugin-dts';
@@ -20,9 +23,17 @@ export default defineConfig({
     outDir: 'dist',
     sourcemap: true,
     lib: {
-      entry: 'src/index.ts',
+      entry: glob.sync(path.resolve(__dirname, 'src/**/*.ts'), {
+        ignore: '**/*.spec.ts',
+      }),
       name: 'presentation-libs',
       formats: ['es'],
     },
+    rollupOptions: {
+      output: {
+        preserveModules: true,
+        preserveModulesRoot: 'src',
+      },
+    },
   },
 });

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/preset-templates",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "scripts": {
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"

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

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 1 - 1
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "7.0.4-RC.0",
+  "version": "7.0.5-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

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

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

Некоторые файлы не были показаны из-за большого количества измененных файлов