Parcourir la source

Merge branch 'master' into feat/page-bulk-export

Futa Arai il y a 1 an
Parent
commit
06c3d84450
46 fichiers modifiés avec 624 ajouts et 721 suppressions
  1. 4 1
      apps/app/nodemon.json
  2. 1 1
      apps/app/package.json
  3. 21 0
      apps/app/playwright/20-basic-features/presentation.spec.ts
  4. 86 0
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  5. 7 7
      apps/app/public/images/logo.svg
  6. 3 31
      apps/app/public/static/locales/en_US/translation.json
  7. 3 31
      apps/app/public/static/locales/fr_FR/translation.json
  8. 3 31
      apps/app/public/static/locales/ja_JP/translation.json
  9. 3 31
      apps/app/public/static/locales/zh_CN/translation.json
  10. 27 4
      apps/app/src/client/components/PageHistory/RevisionDiff.tsx
  11. 3 1
      apps/app/src/client/components/PagePresentationModal.tsx
  12. 1 1
      apps/app/src/client/components/PageTags/TagEditModal.tsx
  13. 3 0
      apps/app/src/client/services/renderer/renderer.tsx
  14. 15 16
      apps/app/src/components/Common/GrowiLogo.jsx
  15. 31 0
      apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx
  16. 2 2
      apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx
  17. 0 19
      apps/app/src/features/comment/server/models/comment.ts
  18. 4 0
      apps/app/src/pages/[[...path]].page.tsx
  19. 6 9
      apps/app/src/server/models/page-tag-relation.ts
  20. 1 0
      apps/app/src/server/models/tag.ts
  21. 6 5
      apps/app/src/server/routes/apiv3/staffs.js
  22. 12 0
      apps/app/src/server/service/config-loader.ts
  23. 1 1
      apps/app/src/server/service/import.js
  24. 139 0
      apps/app/src/server/service/search-delegator/aggregate-to-index.ts
  25. 52 0
      apps/app/src/server/service/search-delegator/bulk-write.d.ts
  26. 43 145
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  27. 7 9
      apps/app/src/server/service/search.ts
  28. 2 1
      apps/app/src/server/service/slack-command-handler/keep.js
  29. 3 2
      apps/app/src/server/service/slack-command-handler/togetter.js
  30. 4 0
      apps/app/src/stores-universal/context.tsx
  31. 2 1
      apps/app/src/stores/admin/customize.tsx
  32. 0 219
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts
  33. 2 2
      packages/core-styles/scss/variables/_growi-official-colors.scss
  34. 3 1
      packages/editor/package.json
  35. 2 0
      packages/editor/src/@types/emoji-mart.d.ts
  36. 23 79
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EmojiButton.tsx
  37. 4 6
      packages/editor/src/client/services-internal/extensions/emojiAutocompletionSettings.ts
  38. 0 3
      packages/editor/vite.config.ts
  39. 3 0
      packages/presentation/package.json
  40. 0 13
      packages/presentation/src/client/components/Presentation.global.scss
  41. 28 0
      packages/presentation/src/client/components/Presentation.module.scss
  42. 4 5
      packages/presentation/src/client/components/Presentation.tsx
  43. 2 1
      packages/presentation/src/client/services/growi-marpit.ts
  44. 1 1
      packages/presentation/src/client/services/renderer/extract-sections.ts
  45. 1 0
      packages/presentation/src/client/services/sanitize-option.ts
  46. 56 42
      yarn.lock

+ 4 - 1
apps/app/nodemon.json

@@ -4,6 +4,9 @@
     ".next",
     "public/static",
     "package.json",
-    "playwright"
+    "playwright",
+    "test",
+    "test-with-vite",
+    "tmp"
   ]
 }

+ 1 - 1
apps/app/package.json

@@ -63,7 +63,7 @@
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^4.0.1",
+    "@azure/identity": "^4.3.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",

+ 21 - 0
apps/app/playwright/20-basic-features/presentation.spec.ts

@@ -0,0 +1,21 @@
+import { test, expect } from '@playwright/test';
+
+test('Presentation', async({ page }) => {
+  await page.goto('/');
+
+  // show presentation modal
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  await page.getByTestId('open-presentation-modal-btn').click();
+
+  // check the content of the h1
+  await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
+    .toHaveText(/Welcome to GROWI/);
+
+  // forward the slide
+  await page.keyboard.press('ArrowRight');
+  await page.keyboard.press('ArrowRight');
+
+  // check the content of the h1
+  await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
+    .toHaveText(/For administrator/);
+});

+ 86 - 0
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -0,0 +1,86 @@
+import { test, expect, type Page } from '@playwright/test';
+
+const openPageItemControl = async(page: Page): Promise<void> => {
+  await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+};
+
+test('Page Deletion and PutBack is executed successfully', async({ page }) => {
+  await page.goto('/Sandbox/Bootstrap5');
+
+  // Delete
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-delete-modal-btn').click();
+  await expect(page.getByTestId('page-delete-modal')).toBeVisible();
+  await page.getByTestId('delete-page-button').click();
+
+  // PutBack
+  await expect(page.getByTestId('trash-page-alert')).toBeVisible();
+  await page.getByTestId('put-back-button').click();
+  await expect(page.getByTestId('put-back-page-modal')).toBeVisible();
+  await page.getByTestId('put-back-execution-button').click();
+  await expect(page.getByTestId('trash-page-alert')).not.toBeVisible();
+});
+
+test('PageDuplicateModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-duplicate-modal-btn').click();
+
+  await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
+});
+
+test('PageMoveRenameModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('rename-page-btn').click();
+
+  await expect(page.getByTestId('page-rename-modal')).toBeVisible();
+});
+
+// TODO: Uncomment after https://redmine.weseek.co.jp/issues/149786
+// test('PresentationModal for "/" is shown successfully', async({ page }) => {
+//   await page.goto('/');
+
+//   await openPageItemControl(page);
+//   await page.getByTestId('open-presentation-modal-btn').click();
+
+//   expect(page.getByTestId('page-presentation-modal')).toBeVisible();
+// });
+
+test.describe('Page Accessories Modal', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+    await openPageItemControl(page);
+  });
+
+  test('Page History is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-history-tab').click();
+    await expect(page.getByTestId(('page-history'))).toBeVisible();
+  });
+
+  test('Page Attachment Data is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-attachment-data-tab').click();
+    await expect(page.getByTestId('page-attachment')).toBeVisible();
+  });
+
+  test('Share Link Management is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+    await expect(page.getByTestId('share-link-management')).toBeVisible();
+  });
+});
+
+test('Successfully add new tag', async({ page }) => {
+  const tag = 'we';
+  await page.goto('/Sandbox/Bootstrap5');
+
+  await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
+  await expect(page.locator('#edit-tag-modal')).toBeVisible();
+  await page.locator('.rbt-input-main').fill(tag);
+  await expect(page.locator('#tag-typeahead-asynctypeahead-item-0')).toBeVisible();
+  await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
+  await page.getByTestId('tag-edit-done-btn').click();
+  await expect(page.getByTestId('grw-tag-labels')).toContainText(tag);
+});

+ 7 - 7
apps/app/public/images/logo.svg

@@ -1,12 +1,12 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 226.44 196.11">
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 56">
   <defs>
     <style>
-      .group1 { fill: #74bc46; }
-      .group2 { fill: #175fa5; }
+      .group1 { fill: #7AD340; }
+      .group2 { fill: #428DD1; }
     </style>
   </defs>
-  <polygon class="group2" points="56.61 196.11 169.83 196.11 226.44 98.06 188.7 98.06 150.96 163.43 75.48 163.43 56.61 196.11" />
-  <polygon class="group1" points="75.48 98.05 94.35 65.37 150.96 65.38 207.57 65.37 207.57 65.38 226.44 98.06 169.83 98.06 113.22 98.06 94.39 130.66 94.3 130.66 84.92 114.4 75.48 98.05" />
-  <polygon class="group1" points="0 98.06 56.6 0 113.22 0.01 169.83 0.01 169.83 0.01 188.69 32.68 132.09 32.69 75.47 32.69 18.86 130.74 0 98.06" />
-  <polygon class="group1" points="75.48 163.43 56.61 130.74 37.71 163.46 47.15 179.81 56.54 196.07 56.63 196.07 75.48 163.43" />
+  <path d="M17.123 33.8015L10.4717 45.3855C10.2686 45.7427 10.2686 46.1829 10.4717 46.5337L15.5934 55.4514C15.7838 55.7767 16.171 55.9999 16.5645 55.9999H17.123L23.5014 44.9007L17.123 33.8015Z" class="group1"/>
+<path d="M50.8118 29.0493L42.0343 44.3331C41.8693 44.6138 41.571 44.9072 41.0632 44.9072H23.4956L17.1172 56H47.4607C47.8542 56 48.1842 55.8023 48.3873 55.4514L63.5559 29.043H50.8118V29.0493Z" class="group2"/>
+<path d="M63.8353 28.5773C64.0447 28.22 64.0574 27.8182 63.8543 27.461L58.7262 18.5369C58.5231 18.1797 58.174 17.9501 57.7615 17.9501H26.8975C26.485 17.9501 26.1106 18.1733 25.9011 18.5178L21.0586 26.9379L27.437 38.0499L32.1272 29.8849C32.4255 29.3746 32.9713 29.0557 33.5552 29.0557H63.5624L63.8353 28.5836V28.5773Z" class="group1"/>
+<path d="M22.956 11.0992H54.4546L48.4125 0.580476C48.2094 0.22326 47.8604 0 47.4478 0H16.5839C16.1714 0 15.7969 0.204123 15.5875 0.56134L0.152321 27.4227C-0.0507735 27.7799 -0.0507735 28.2137 0.152321 28.5709L6.20706 39.1088L21.9595 11.6606C22.1626 11.3033 22.5434 11.0928 22.956 11.0928V11.0992Z" class="group1"/>
 </svg>

+ 3 - 31
apps/app/public/static/locales/en_US/translation.json

@@ -321,7 +321,9 @@
       "stale": "More than {{count}} year has passed since last update.",
       "stale_plural": "More than {{count}} years has passed since last update.",
       "expiration": "This share link will expire at <strong>{{expiredAt}}</strong>.",
-      "no_deadline": "This page has no expiration date"
+      "no_deadline": "This page has no expiration date",
+      "not_indexed1": "This page may not be indexed by Full-Text search engines.",
+      "not_indexed2": "Page body exceeds the threshold specified by {{threshold}}."
     }
   },
   "page_edit": {
@@ -713,36 +715,6 @@
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match",
     "please_enable_mailer_alert": "The password reset feature is disabled because email setup has not been completed. Please ask administrator to complete the email setup."
   },
-  "emoji": {
-    "title": "Pick an Emoji",
-    "search": "Search",
-    "clear": "Clear",
-    "notfound": "No Emoji Found",
-    "skintext": "Choose your default skin tone",
-    "categories": {
-      "search": "Search Results",
-      "recent": "Frequently Used",
-      "smileys": "Smileys & Emotion",
-      "people": "People & Body",
-      "nature": "Animals & Nature",
-      "foods": "Food & Drink",
-      "activity": "Activity",
-      "places": "Travel & Places",
-      "objects": "Objects",
-      "symbols": "Symbols",
-      "flags": "Flags",
-      "custom": "Custom"
-    },
-    "categorieslabel": "Emoji categories",
-    "skintones": {
-      "1": "Default Skin Tone",
-      "2": "Light Skin Tone",
-      "3": "Medium-Light Skin Tone",
-      "4": "Medium Skin Tone",
-      "5": "Medium-Dark Skin Tone",
-      "6": "Dark Skin Tone"
-    }
-  },
   "maintenance_mode": {
     "maintenance_mode": "Maintenance Mode",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",

+ 3 - 31
apps/app/public/static/locales/fr_FR/translation.json

@@ -321,7 +321,9 @@
       "stale": "Plus de {{count}} an est passé depuis la dernière mise à jour.",
       "stale_plural": "Plus de {{count}} années sont passées depuis la dernière mise à jour.",
       "expiration": "Ce lien expirera <strong>{{expiredAt}}</strong>.",
-      "no_deadline": "Cette page n'a pas de date d'expiration"
+      "no_deadline": "Cette page n'a pas de date d'expiration",
+      "not_indexed1": "Cette page n'est peut-être pas indexée par les moteurs de recherche Full-Text.",
+      "not_indexed2": "Le corps de la page dépasse le seuil spécifié par {{threshold}}."
     }
   },
   "page_edit": {
@@ -707,36 +709,6 @@
     "password_and_confirm_password_does_not_match": "Le mot de passe ne correspond pas",
     "please_enable_mailer_alert": "La réinitialisation de mot de passe est désactivée, car la configuration d'envois de courriels est incomplète."
   },
-  "emoji": {
-    "title": "Choisir un émoji",
-    "search": "Rechercher",
-    "clear": "Vider",
-    "notfound": "Aucun émoji trouvé",
-    "skintext": "Choisir le teint par défaut",
-    "categories": {
-      "search": "Résultats de recherche",
-      "recent": "Récents",
-      "smileys": "Émotions",
-      "people": "Individus & corps",
-      "nature": "Animaux & nature",
-      "foods": "Nourriture & boisson",
-      "activity": "Activités",
-      "places": "Voyage",
-      "objects": "Objets",
-      "symbols": "Symboles",
-      "flags": "Drapeaux",
-      "custom": "Personnalisé"
-    },
-    "categorieslabel": "Catégories d'émojis",
-    "skintones": {
-      "1": "Teint par défaut",
-      "2": "Teint clair",
-      "3": "Teint moyen-clair",
-      "4": "Teint moyen",
-      "5": "Teint moyen-foncé",
-      "6": "Teint foncé"
-    }
-  },
   "maintenance_mode": {
     "maintenance_mode": "Mode maintenance",
     "growi_is_under_maintenance": "GROWI est actuellement en maintenance.",

+ 3 - 31
apps/app/public/static/locales/ja_JP/translation.json

@@ -354,7 +354,9 @@
       "restricted": "このページの閲覧は制限されています",
       "stale": "このページは最終更新日から{{count}}年以上が経過しています。",
       "expiration": "この共有パーマリンクの有効期限は <strong>{{expiredAt}}</strong> です。",
-      "no_deadline": "このページに有効期限は設定されていません。"
+      "no_deadline": "このページに有効期限は設定されていません。",
+      "not_indexed1": "このページは全文検索エンジンにインデックスされない可能性があります.",
+      "not_indexed2": "ページ本文が閾値を超えています: {{threshold}}."
     }
   },
   "page_edit": {
@@ -746,36 +748,6 @@
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません",
     "please_enable_mailer_alert": "メール設定が完了していないため、パスワード再設定機能が無効になっています。メール設定を完了させるよう管理者に依頼してください。"
   },
-  "emoji": {
-    "title": "絵文字を選択",
-    "search": "探す",
-    "clear": "リセット",
-    "notfound": "絵文字が見つかりません",
-    "skintext": "デフォルトの肌の色を選択",
-    "categories": {
-      "search": "検索結果",
-      "recent": "最新履歴",
-      "smileys": "スマイリーと感情",
-      "people": "人と体",
-      "nature": "動物と自然",
-      "foods": "食べ物や飲み物",
-      "activity": "アクティビティ",
-      "places": "旅行と場所",
-      "objects": "オブジェクト",
-      "symbols": "シンボル",
-      "flags": "国旗",
-      "custom": "カスタマイズ"
-    },
-    "categorieslabel": "絵文字カテゴリ",
-    "skintones": {
-      "1": "デフォルトの肌の色",
-      "2": "明るい肌のトーン",
-      "3": "ミディアム-明るい肌のトーン",
-      "4": "ミディアムスキントーン",
-      "5": "ミディアムダークスキントーン",
-      "6": "肌の色が濃い"
-    }
-  },
   "maintenance_mode": {
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",

+ 3 - 31
apps/app/public/static/locales/zh_CN/translation.json

@@ -311,7 +311,9 @@
       "restricted": "访问此页受到限制",
       "stale": "自上次更新以来,已超过{{count}年。",
       "stale_plural": "自上次更新以来已过去{{count}年以上。",
-      "no_deadline": "This page has no expiration date"
+      "no_deadline": "此页面没有到期日期",
+      "not_indexed1": "此页面可能不会被全文搜索引擎索引。",
+      "not_indexed2": "页面正文超过了{{threshold}}指定的阈值。"
 		}
 	},
 	"page_edit": {
@@ -716,36 +718,6 @@
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配",
     "please_enable_mailer_alert": "密码重置功能被禁用,因为电子邮件设置尚未完成。请要求管理员完成电子邮件的设置。"
   },
-  "emoji": {
-    "title": "选择一个表情符号",
-    "search": "搜索",
-    "clear": "重置",
-    "notfound": "找不到表情符号",
-    "skintext": "选择您的默认肤色",
-    "categories": {
-      "search": "搜索结果",
-      "recent": "经常使用",
-      "smileys": "笑脸和情感",
-      "people": "人和身体",
-      "nature": "动物与自然",
-      "foods": "食物和饮料",
-      "activity": "活动",
-      "places": "旅行和地方",
-      "objects": "对象",
-      "symbols": "符号",
-      "flags": "旗帜",
-      "custom": "定制"
-    },
-    "categorieslabel": "表情符号类别",
-    "skintones": {
-      "1": "默认肤色",
-      "2": "浅肤色",
-      "3": "中浅肤色",
-      "4": "中等肤色",
-      "5": "中深肤色",
-      "6": "深色肤色"
-    }
-  },
   "maintenance_mode": {
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",

+ 27 - 4
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -1,7 +1,9 @@
 import { useMemo } from 'react';
 
 import type { IRevisionHasPageId } from '@growi/core';
+import { GrowiThemeSchemeType } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
+import { PresetThemesMetadatas } from '@growi/preset-themes';
 import { createPatch } from 'diff';
 import type { Diff2HtmlConfig } from 'diff2html';
 import { html } from 'diff2html';
@@ -10,14 +12,19 @@ import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import urljoin from 'url-join';
 
+
 import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 
 import UserDate from '../../../components/User/UserDate';
+import { useSWRxGrowiThemeSetting } from '../../../stores/admin/customize';
+
 
 import styles from './RevisionDiff.module.scss';
 
 import 'diff2html/bundles/css/diff2html.min.css';
 
+const moduleClass = styles['revision-diff-container'];
+
 type RevisioinDiffProps = {
   currentRevision: IRevisionHasPageId,
   previousRevision: IRevisionHasPageId,
@@ -34,10 +41,26 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
     currentRevision, previousRevision, revisionDiffOpened, currentPageId, currentPagePath, onClose,
   } = props;
 
-  const { theme } = useNextThemes();
+  const { theme: userTheme } = useNextThemes();
+  const { data: growiTheme } = useSWRxGrowiThemeSetting();
 
   const colorScheme: ColorSchemeType = useMemo(() => {
-    switch (theme) {
+    if (growiTheme == null) {
+      return ColorSchemeType.AUTO;
+    }
+
+    const growiThemeSchemeType = growiTheme.pluginThemesMetadatas[0]?.schemeType
+        ?? PresetThemesMetadatas.find(theme => theme.name === growiTheme.currentTheme)?.schemeType;
+
+    switch (growiThemeSchemeType) {
+      case GrowiThemeSchemeType.DARK:
+        return ColorSchemeType.DARK;
+      case GrowiThemeSchemeType.LIGHT:
+        return ColorSchemeType.LIGHT;
+      default:
+        // growiThemeSchemeType === GrowiThemeSchemeType.BOTH
+    }
+    switch (userTheme) {
       case Themes.DARK:
         return ColorSchemeType.DARK;
       case Themes.LIGHT:
@@ -45,7 +68,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
       default:
         return ColorSchemeType.AUTO;
     }
-  }, [theme]);
+  }, [growiTheme, userTheme]);
 
   const previousText = (currentRevision._id === previousRevision._id) ? '' : previousRevision.body;
 
@@ -66,7 +89,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
   const diffView = { __html: diffViewHTML };
 
   return (
-    <div className={`${styles['revision-diff-container']}`}>
+    <div className={moduleClass}>
       <div className="container">
         <div className="row mt-2">
           <div className="col px-0 py-2">

+ 3 - 1
apps/app/src/client/components/PagePresentationModal.tsx

@@ -19,6 +19,8 @@ import { usePresentationViewOptions } from '~/stores/renderer';
 
 import styles from './PagePresentationModal.module.scss';
 
+const moduleClass = styles['grw-presentation-modal'] ?? '';
+
 
 const Presentation = dynamic<PresentationProps>(() => import('./Presentation/Presentation').then(mod => mod.Presentation), {
   ssr: false,
@@ -71,7 +73,7 @@ const PagePresentationModal = (): JSX.Element => {
       isOpen={isOpen}
       toggle={closeHandler}
       data-testid="page-presentation-modal"
-      className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
+      className={moduleClass}
     >
       <div className="grw-presentation-controls d-flex">
         <button

+ 1 - 1
apps/app/src/client/components/PageTags/TagEditModal.tsx

@@ -59,7 +59,7 @@ const TagEditModalSubstance: React.FC<TagEditModalSubstanceProps> = (props: TagE
         <TagsInput tags={initTags} onTagsUpdated={tags => setTags(tags)} autoFocus />
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-primary" onClick={handleSubmit}>
+        <button type="button" data-testid="tag-edit-done-btn" className="btn btn-primary" onClick={handleSubmit}>
           {t('tag_edit_modal.done')}
         </button>
       </ModalFooter>

+ 3 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,6 +1,7 @@
 import assert from 'assert';
 
 import { isClient } from '@growi/core/dist/utils/browser-utils';
+import * as presentation from '@growi/presentation/dist/client/services/sanitize-option';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
 import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
@@ -78,6 +79,7 @@ export const generateViewOptions = (
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       commonSanitizeOption,
+      presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,
@@ -192,6 +194,7 @@ export const generateSimpleViewOptions = (
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       commonSanitizeOption,
+      presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,

+ 15 - 16
apps/app/src/components/Common/GrowiLogo.jsx

@@ -10,29 +10,28 @@ const GrowiLogo = memo(() => (
       xmlns="http://www.w3.org/2000/svg"
       width="32"
       height="32"
-      viewBox="0 0 226.44 196.11"
+      viewBox="0 0 64 56"
     >
       <path
-        d="M56.61 196.11L169.83 196.11 226.44 98.06 188.7 98.06 150.96 163.43 75.48 163.43 56.61 196.11z"
-        className="group2"
-      >
-      </path>
-      <path
-      // eslint-disable-next-line max-len
-        d="M75.48 98.05L94.35 65.37 150.96 65.38 207.57 65.37 207.57 65.38 226.44 98.06 169.83 98.06 113.22 98.06 94.39 130.66 94.3 130.66 84.92 114.4 75.48 98.05z"
+        // eslint-disable-next-line max-len
+        d="M17.123 33.8015L10.4717 45.3855C10.2686 45.7427 10.2686 46.1829 10.4717 46.5337L15.5934 55.4514C15.7838 55.7767 16.171 55.9999 16.5645 55.9999H17.123L23.5014 44.9007L17.123 33.8015Z"
         className="group1"
-      >
-      </path>
+      />
+      <path
+        // eslint-disable-next-line max-len
+        d="M50.8118 29.0493L42.0343 44.3331C41.8693 44.6138 41.571 44.9072 41.0632 44.9072H23.4956L17.1172 56H47.4607C47.8542 56 48.1842 55.8023 48.3873 55.4514L63.5559 29.043H50.8118V29.0493Z"
+        className="group2"
+      />
       <path
-        d="M0 98.06L56.6 0 113.22 0.01 169.83 0.01 169.83 0.01 188.69 32.68 132.09 32.69 75.47 32.69 18.86 130.74 0 98.06z"
+        // eslint-disable-next-line max-len
+        d="M63.8353 28.5773C64.0447 28.22 64.0574 27.8182 63.8543 27.461L58.7262 18.5369C58.5231 18.1797 58.174 17.9501 57.7615 17.9501H26.8975C26.485 17.9501 26.1106 18.1733 25.9011 18.5178L21.0586 26.9379L27.437 38.0499L32.1272 29.8849C32.4255 29.3746 32.9713 29.0557 33.5552 29.0557H63.5624L63.8353 28.5836V28.5773Z"
         className="group1"
-      >
-      </path>
+      />
       <path
-        d="M75.48 163.43L56.61 130.74 37.71 163.46 47.15 179.81 56.54 196.07 56.63 196.07 75.48 163.43z"
+        // eslint-disable-next-line max-len
+        d="M22.956 11.0992H54.4546L48.4125 0.580476C48.2094 0.22326 47.8604 0 47.4478 0H16.5839C16.1714 0 15.7969 0.204123 15.5875 0.56134L0.152321 27.4227C-0.0507735 27.7799 -0.0507735 28.2137 0.152321 28.5709L6.20706 39.1088L21.9595 11.6606C22.1626 11.3033 22.5434 11.0928 22.956 11.0928V11.0992Z"
         className="group1"
-      >
-      </path>
+      />
     </svg>
   </div>
 ));

+ 31 - 0
apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx

@@ -0,0 +1,31 @@
+import { useTranslation } from 'react-i18next';
+
+import { useElasticsearchMaxBodyLengthToIndex } from '~/stores-universal/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+
+export const FullTextSearchNotCoverAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { data: elasticsearchMaxBodyLengthToIndex } = useElasticsearchMaxBodyLengthToIndex();
+  const { data } = useSWRxCurrentPage();
+
+  const markdownLength = data?.revision?.body?.length;
+
+  if (markdownLength == null || elasticsearchMaxBodyLengthToIndex == null || markdownLength <= elasticsearchMaxBodyLengthToIndex) {
+    return <></>;
+  }
+
+  return (
+    <div className="alert alert-warning">
+      <strong>{t('Warning')}: {t('page_page.notice.not_indexed1')}</strong><br />
+      <small
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{
+          __html: t('page_page.notice.not_indexed2', {
+            threshold: `<code>ELASTICSEARCH_MAX_BODY_LENGTH_TO_INDEX=${elasticsearchMaxBodyLengthToIndex}</code>`,
+          }),
+        }}
+      />
+    </div>
+  );
+};

+ 2 - 2
apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx

@@ -1,5 +1,3 @@
-import React from 'react';
-
 import dynamic from 'next/dynamic';
 
 import { useIsNotFound } from '~/stores/page';
@@ -9,6 +7,7 @@ import { PageGrantAlert } from './PageGrantAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { WipPageAlert } from './WipPageAlert';
 
+const FullTextSearchNotCoverAlert = dynamic(() => import('./FullTextSearchNotCoverAlert').then(mod => mod.FullTextSearchNotCoverAlert), { ssr: false });
 const PageRedirectedAlert = dynamic(() => import('./PageRedirectedAlert').then(mod => mod.PageRedirectedAlert), { ssr: false });
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
@@ -22,6 +21,7 @@ export const PageAlerts = (): JSX.Element => {
       <div className="col-sm-12">
         {/* alerts */}
         { !isNotFound && <FixPageGrantAlert /> }
+        <FullTextSearchNotCoverAlert />
         <WipPageAlert />
         <PageGrantAlert />
         <TrashPageAlert />

+ 0 - 19
apps/app/src/features/comment/server/models/comment.ts

@@ -27,7 +27,6 @@ type Add = (
 type FindCommentsByPageId = (pageId: Types.ObjectId) => Query<CommentDocument[], CommentDocument>;
 type FindCommentsByRevisionId = (revisionId: Types.ObjectId) => Query<CommentDocument[], CommentDocument>;
 type FindCreatorsByPage = (pageId: Types.ObjectId) => Promise<IUser[]>
-type GetPageIdToCommentMap = (pageIds: Types.ObjectId[]) => Promise<Record<string, CommentDocument[]>>
 type CountCommentByPageId = (pageId: Types.ObjectId) => Promise<number>
 
 export interface CommentModel extends Model<CommentDocument> {
@@ -35,7 +34,6 @@ export interface CommentModel extends Model<CommentDocument> {
   findCommentsByPageId: FindCommentsByPageId
   findCommentsByRevisionId: FindCommentsByRevisionId
   findCreatorsByPage: FindCreatorsByPage
-  getPageIdToCommentMap: GetPageIdToCommentMap
   countCommentByPageId: CountCommentByPageId
 }
 
@@ -91,23 +89,6 @@ commentSchema.statics.findCreatorsByPage = async function(page) {
   return this.distinct('creator', { page }).exec();
 };
 
-/**
- * @return {object} key: page._id, value: comments
- */
-commentSchema.statics.getPageIdToCommentMap = async function(pageIds) {
-  const results = await this.aggregate()
-    .match({ page: { $in: pageIds } })
-    .group({ _id: '$page', comments: { $push: '$comment' } });
-
-  // convert to map
-  const idToCommentMap = {};
-  results.forEach((result, i) => {
-    idToCommentMap[result._id] = result.comments;
-  });
-
-  return idToCommentMap;
-};
-
 commentSchema.statics.countCommentByPageId = async function(page) {
   return this.count({ page });
 };

+ 4 - 0
apps/app/src/pages/[[...path]].page.tsx

@@ -42,6 +42,7 @@ import {
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useIsAllReplyShown, useIsContainerFluid, useIsNotCreatable,
   useIsUploadAllFileAllowed, useIsUploadEnabled,
+  useElasticsearchMaxBodyLengthToIndex,
 } from '~/stores-universal/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
@@ -157,6 +158,7 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
+  elasticsearchMaxBodyLengthToIndex: number,
   isEnabledMarp: boolean,
 
   sidebarConfig: ISidebarConfig,
@@ -215,6 +217,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsEnabledAttachTitleHeader(props.isEnabledAttachTitleHeader);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useElasticsearchMaxBodyLengthToIndex(props.elasticsearchMaxBodyLengthToIndex);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
   useIsSlackConfigured(props.isSlackConfigured);
@@ -537,6 +540,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig('crowi', 'app:elasticsearchMaxBodyLengthToIndex');
 
   props.isSlackConfigured = crowi.slackIntegrationService.isSlackConfigured;
   // props.isMailerSetup = mailService.isMailerSetup;

+ 6 - 9
apps/app/src/server/models/page-tag-relation.ts

@@ -1,6 +1,6 @@
 import type { ITag } from '@growi/core';
-import type { Document, Model } from 'mongoose';
-import mongoose, { ObjectId } from 'mongoose';
+import type { Document, Model, ObjectId } from 'mongoose';
+import mongoose from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 
@@ -9,13 +9,10 @@ import type { IPageTagRelation } from '~/interfaces/page-tag-relation';
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-import type { IdToNameMap } from './tag';
+import type { IdToNameMap, IdToNamesMap } from './tag';
 import Tag from './tag';
 
 
-const ObjectId = mongoose.Schema.Types.ObjectId;
-
-
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
@@ -36,7 +33,7 @@ type CreateTagListWithCountResult = {
 }
 type CreateTagListWithCount = (this: PageTagRelationModel, opts?: CreateTagListWithCountOpts) => Promise<CreateTagListWithCountResult>;
 
-type GetIdToTagNamesMap = (this: PageTagRelationModel, pageIds: string[]) => Promise<IdToNameMap>;
+type GetIdToTagNamesMap = (this: PageTagRelationModel, pageIds: string[]) => Promise<IdToNamesMap>;
 
 type UpdatePageTags = (this: PageTagRelationModel, pageId: string, tags: string[]) => Promise<void>
 
@@ -54,13 +51,13 @@ export interface PageTagRelationModel extends Model<PageTagRelationDocument> {
  */
 const schema = new mongoose.Schema<PageTagRelationDocument, PageTagRelationModel>({
   relatedPage: {
-    type: ObjectId,
+    type: mongoose.Schema.Types.ObjectId,
     ref: 'Page',
     required: true,
     index: true,
   },
   relatedTag: {
-    type: ObjectId,
+    type: mongoose.Schema.Types.ObjectId,
     ref: 'Tag',
     required: true,
     index: true,

+ 1 - 0
apps/app/src/server/models/tag.ts

@@ -14,6 +14,7 @@ export interface TagDocument {
 }
 
 export type IdToNameMap = {[key: string] : string }
+export type IdToNamesMap = {[key: string] : string[] }
 
 export interface TagModel extends Model<TagDocument>{
   getIdToNameMap(tagIds: ObjectIdLike[]): IdToNameMap

+ 6 - 5
apps/app/src/server/routes/apiv3/staffs.js

@@ -1,13 +1,14 @@
+import axios from 'axios';
+import { addHours } from 'date-fns/addHours';
+import { isAfter } from 'date-fns/isAfter';
+import { Router } from 'express';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:staffs'); // eslint-disable-line no-unused-vars
 
-const express = require('express');
-
-const axios = require('axios');
 
-const router = express.Router();
-const { isAfter, addHours } = require('date-fns');
+const router = Router();
 
 const contributors = require('^/resource/Contributor');
 

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

@@ -282,6 +282,18 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.BOOLEAN,
     default: false,
   },
+  ELASTICSEARCH_MAX_BODY_LENGTH_TO_INDEX: {
+    ns:      'crowi',
+    key:     'app:elasticsearchMaxBodyLengthToIndex',
+    type:    ValueType.NUMBER,
+    default: 100000,
+  },
+  ELASTICSEARCH_REINDEX_BULK_SIZE: {
+    ns:      'crowi',
+    key:     'app:elasticsearchReindexBulkSize',
+    type:    ValueType.NUMBER,
+    default: 100,
+  },
   ELASTICSEARCH_REINDEX_ON_BOOT: {
     ns:      'crowi',
     key:     'app:elasticsearchReindexOnBoot',

+ 1 - 1
apps/app/src/server/service/import.js

@@ -3,6 +3,7 @@
  * @typedef {import("@types/unzip-stream").Entry} Entry
  */
 
+import { parseISO } from 'date-fns/parseISO';
 import gc from 'expose-gc/function';
 
 import loggerFactory from '~/utils/logger';
@@ -14,7 +15,6 @@ const path = require('path');
 const { Writable, Transform } = require('stream');
 
 const JSONStream = require('JSONStream');
-const parseISO = require('date-fns/parseISO');
 const isIsoDate = require('is-iso-date');
 const mongoose = require('mongoose');
 const streamToPromise = require('stream-to-promise');

+ 139 - 0
apps/app/src/server/service/search-delegator/aggregate-to-index.ts

@@ -0,0 +1,139 @@
+import type { IPage } from '@growi/core';
+import type { PipelineStage, Query } from 'mongoose';
+
+import type { PageModel } from '~/server/models/page';
+
+export const aggregatePipelineToIndex = (maxBodyLengthToIndex: number, query?: Query<PageModel, IPage>): PipelineStage[] => {
+
+  const basePipeline = query == null
+    ? []
+    : [{ $match: query.getQuery() }];
+
+  return [
+    ...basePipeline,
+
+    // join Revision
+    {
+      $lookup: {
+        from: 'revisions',
+        localField: 'revision',
+        foreignField: '_id',
+        as: 'revision',
+      },
+    },
+    // unwind and filter pages that does not have revision
+    {
+      $unwind: {
+        path: '$revision',
+      },
+    },
+    {
+      $addFields: {
+        bodyLength: { $strLenCP: '$revision.body' },
+      },
+    },
+
+    // join User
+    {
+      $lookup: {
+        from: 'users',
+        localField: 'creator',
+        foreignField: '_id',
+        as: 'creator',
+      },
+    },
+    {
+      $unwind: {
+        path: '$creator',
+        preserveNullAndEmptyArrays: true,
+      },
+    },
+
+    // join Comment
+    {
+      $lookup: {
+        from: 'comments',
+        localField: '_id',
+        foreignField: 'page',
+        pipeline: [
+          {
+            $addFields: {
+              commentLength: { $strLenCP: '$comment' },
+            },
+          },
+        ],
+        as: 'comments',
+      },
+    },
+    {
+      $addFields: {
+        commentsCount: { $size: '$comments' },
+      },
+    },
+
+    // join Bookmark
+    {
+      $lookup: {
+        from: 'bookmarks',
+        localField: '_id',
+        foreignField: 'page',
+        as: 'bookmarks',
+      },
+    },
+    {
+      $addFields: {
+        bookmarksCount: { $size: '$bookmarks' },
+      },
+    },
+
+    // add counts for embedded arrays
+    {
+      $addFields: {
+        likeCount: { $size: '$liker' },
+      },
+    },
+    {
+      $addFields: {
+        seenUsersCount: { $size: '$seenUsers' },
+      },
+    },
+
+    // project
+    {
+      $project: {
+        path: 1,
+        createdAt: 1,
+        updatedAt: 1,
+        grant: 1,
+        grantedUsers: 1,
+        grantedGroups: 1,
+        'revision.body': {
+          $cond: {
+            if: { $lte: ['$bodyLength', maxBodyLengthToIndex] },
+            then: '$revision.body',
+            else: '',
+          },
+        },
+        comments: {
+          $map: {
+            input: '$comments',
+            as: 'comment',
+            in: {
+              $cond: {
+                if: { $lte: ['$$comment.commentLength', maxBodyLengthToIndex] },
+                then: '$$comment.comment',
+                else: '',
+              },
+            },
+          },
+        },
+        commentsCount: 1,
+        bookmarksCount: 1,
+        likeCount: 1,
+        seenUsersCount: 1,
+        'creator.username': 1,
+        'creator.email': 1,
+      },
+    },
+  ];
+};

+ 52 - 0
apps/app/src/server/service/search-delegator/bulk-write.d.ts

@@ -0,0 +1,52 @@
+import type { IPageHasId, PageGrant } from '@growi/core';
+
+export type AggregatedPage = Pick<IPageHasId,
+  '_id'
+  | 'path'
+  | 'createdAt'
+  | 'updatedAt'
+  | 'grant'
+  | 'grantedUsers'
+  | 'grantedGroups'
+> & {
+  revision: { body: string },
+  comments: string[],
+  commentsCount: number,
+  bookmarksCount: number,
+  likeCount: number,
+  seenUsersCount: number,
+  creator: {
+    username: string,
+    email: string,
+  },
+} & {
+  tagNames: string[],
+};
+
+export type BulkWriteCommand = {
+  index: {
+    _index: string,
+    _type: '_doc' | undefined,
+    _id: string,
+  },
+}
+
+export type BulkWriteBodyRestriction = {
+  grant: PageGrant,
+  granted_users?: string[],
+  granted_groups: string[],
+}
+
+export type BulkWriteBody = {
+  path: string;
+  created_at: Date;
+  updated_at: Date;
+  body: string;
+  username?: string;
+  comments?: string[];
+  comment_count: number;
+  bookmark_count: number;
+  seenUsers_count: number;
+  like_count: number;
+  tag_names?: string[];
+} & BulkWriteBodyRestriction;

+ 43 - 145
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -1,11 +1,11 @@
 import { Writable, Transform } from 'stream';
 import { URL } from 'url';
 
+import { getIdForRef, type IPage } from '@growi/core';
 import gc from 'expose-gc/function';
 import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
-import { Comment } from '~/features/comment/server';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import type { ISearchResult, ISearchResultData } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
@@ -18,16 +18,18 @@ import type {
 } from '../../interfaces/search';
 import type { PageModel } from '../../models/page';
 import { createBatchStream } from '../../util/batch-stream';
+import { configManager } from '../config-manager';
 import type { UpdateOrInsertPagesOpts } from '../interfaces/search';
 
 
+import { aggregatePipelineToIndex } from './aggregate-to-index';
+import type { AggregatedPage, BulkWriteBody, BulkWriteCommand } from './bulk-write';
 import ElasticsearchClient from './elasticsearch-client';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
 const DEFAULT_OFFSET = 0;
 const DEFAULT_LIMIT = 50;
-const BULK_REINDEX_SIZE = 100;
 
 const { RELATION_SCORE, CREATED_AT, UPDATED_AT } = SORT_AXIS;
 const { DESC, ASC } = SORT_ORDER;
@@ -68,12 +70,11 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   esUri: string;
 
-  constructor(configManager, socketIoService) {
+  constructor(socketIoService) {
     this.name = SearchDelegatorName.DEFAULT;
-    this.configManager = configManager;
     this.socketIoService = socketIoService;
 
-    const elasticsearchVersion: number = this.configManager.getConfig('crowi', 'app:elasticsearchVersion');
+    const elasticsearchVersion: number = configManager.getConfig('crowi', 'app:elasticsearchVersion');
 
     if (elasticsearchVersion !== 7 && elasticsearchVersion !== 8) {
       throw new Error('Unsupported Elasticsearch version. Please specify a valid number to \'ELASTICSEARCH_VERSION\'');
@@ -81,7 +82,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
-    this.isElasticsearchReindexOnBoot = this.configManager.getConfig('crowi', 'app:elasticsearchReindexOnBoot');
+    this.isElasticsearchReindexOnBoot = configManager.getConfig('crowi', 'app:elasticsearchReindexOnBoot');
 
     // In Elasticsearch RegExp, we don't need to used ^ and $.
     // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-regexp-query.html#_standard_operators
@@ -110,26 +111,22 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return `${this.indexName}-alias`;
   }
 
-  shouldIndexed(page) {
-    return page.revision != null;
-  }
-
   initClient() {
     const { host, auth, indexName } = this.getConnectionInfo();
 
-    const rejectUnauthorized = this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized');
+    const rejectUnauthorized = configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized');
 
     const options = {
       node: host,
       auth,
-      requestTimeout: this.configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
+      requestTimeout: configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
     };
 
     this.client = new ElasticsearchClient(this.isElasticsearchV7, options, rejectUnauthorized);
     this.indexName = indexName;
   }
 
-  getType() {
+  getType(): '_doc' | undefined {
     return this.isElasticsearchV7 ? '_doc' : undefined;
   }
 
@@ -142,7 +139,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     let host = this.esUri;
     let auth;
 
-    const elasticsearchUri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
+    const elasticsearchUri = configManager.getConfig('crowi', 'app:elasticsearchUri');
 
     const url = new URL(elasticsearchUri);
     if (url.pathname !== '/') {
@@ -359,20 +356,9 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   /**
    * generate object that is related to page.grant*
    */
-  generateDocContentsRelatedToRestriction(page) {
-    let grantedUserIds = null;
-    if (page.grantedUsers != null && page.grantedUsers.length > 0) {
-      grantedUserIds = page.grantedUsers.map((user) => {
-        const userId = (user._id == null) ? user : user._id;
-        return userId.toString();
-      });
-    }
-
-    let grantedGroupIds = [];
-    grantedGroupIds = page.grantedGroups.map((group) => {
-      const groupId = (group.item._id == null) ? group.item : group.item._id;
-      return groupId.toString();
-    });
+  generateDocContentsRelatedToRestriction(page: AggregatedPage) {
+    const grantedUserIds = page.grantedUsers.map(user => getIdForRef(user));
+    const grantedGroupIds = page.grantedGroups.map(group => getIdForRef(group.item));
 
     return {
       grant: page.grant,
@@ -381,10 +367,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     };
   }
 
-  prepareBodyForCreate(body, page) {
-    if (!Array.isArray(body)) {
-      throw new Error('Body must be an array.');
-    }
+  prepareBodyForCreate(page: AggregatedPage): [BulkWriteCommand, BulkWriteBody] {
 
     const command = {
       index: {
@@ -394,27 +377,22 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       },
     };
 
-    const bookmarkCount = page.bookmarkCount || 0;
-    const seenUsersCount = page.seenUsers?.length || 0;
-    let document = {
+    const document: BulkWriteBody = {
       path: page.path,
       body: page.revision.body,
-      // username: page.creator?.username, // available Node.js v14 and above
-      username: page.creator != null ? page.creator.username : null,
-      comments: page.comments,
-      comment_count: page.commentCount,
-      bookmark_count: bookmarkCount,
-      seenUsers_count: seenUsersCount,
-      like_count: page.liker?.length || 0,
+      username: page.creator?.username,
+      comments: page.commentsCount > 0 ? page.comments : undefined,
+      comment_count: page.commentsCount,
+      bookmark_count: page.bookmarksCount,
+      like_count: page.likeCount,
+      seenUsers_count: page.seenUsersCount,
       created_at: page.createdAt,
       updated_at: page.updatedAt,
       tag_names: page.tagNames,
+      ...this.generateDocContentsRelatedToRestriction(page),
     };
 
-    document = Object.assign(document, this.generateDocContentsRelatedToRestriction(page));
-
-    body.push(command);
-    body.push(document);
+    return [command, document];
   }
 
   prepareBodyForDelete(body, page) {
@@ -457,89 +435,28 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   async updateOrInsertPages(queryFactory, option: UpdateOrInsertPagesOpts = {}) {
     const { shouldEmitProgress = false, invokeGarbageCollection = false } = option;
 
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
-    const Bookmark = mongoose.model('Bookmark') as any; // TODO: typescriptize model
 
     const socket = shouldEmitProgress ? this.socketIoService.getAdminSocket() : null;
 
     // prepare functions invoked from custom streams
     const prepareBodyForCreate = this.prepareBodyForCreate.bind(this);
-    const shouldIndexed = this.shouldIndexed.bind(this);
     const bulkWrite = this.client.bulk.bind(this.client);
 
-    const findQuery = new PageQueryBuilder(queryFactory()).query;
-    const countQuery = new PageQueryBuilder(queryFactory()).query;
+    const matchQuery = new PageQueryBuilder(queryFactory()).query;
 
+    const countQuery = new PageQueryBuilder(queryFactory()).query;
     const totalCount = await countQuery.count();
 
-    const readStream = findQuery
-      // populate data which will be referenced by prepareBodyForCreate()
-      .populate([
-        { path: 'creator', model: 'User', select: 'username' },
-        { path: 'revision', model: 'Revision', select: 'body' },
-      ])
-      .lean()
-      .cursor();
-
-    let skipped = 0;
-    const thinOutStream = new Transform({
-      objectMode: true,
-      async transform(doc, encoding, callback) {
-        if (shouldIndexed(doc)) {
-          this.push(doc);
-        }
-        else {
-          skipped++;
-        }
-        callback();
-      },
-    });
-
-    const batchStream = createBatchStream(BULK_REINDEX_SIZE);
-
-    const appendBookmarkCountStream = new Transform({
-      objectMode: true,
-      async transform(chunk, encoding, callback) {
-        const pageIds = chunk.map(doc => doc._id);
+    const maxBodyLengthToIndex = configManager.getConfig('crowi', 'app:elasticsearchMaxBodyLengthToIndex');
 
-        const idToCountMap = await Bookmark.getPageIdToCountMap(pageIds);
-        const idsHavingCount = Object.keys(idToCountMap);
+    const readStream = Page.aggregate<AggregatedPage>(
+      aggregatePipelineToIndex(maxBodyLengthToIndex, matchQuery),
+    ).cursor();
 
-        // append count
-        chunk
-          .filter(doc => idsHavingCount.includes(doc._id.toString()))
-          .forEach((doc) => {
-            // append count from idToCountMap
-            doc.bookmarkCount = idToCountMap[doc._id.toString()];
-          });
-
-        this.push(chunk);
-        callback();
-      },
-    });
-
-
-    const appendCommentStream = new Transform({
-      objectMode: true,
-      async transform(chunk, encoding, callback) {
-        const pageIds = chunk.map(doc => doc._id);
-
-        const idToCommentMap = await Comment.getPageIdToCommentMap(pageIds);
-        const idsHavingComment = Object.keys(idToCommentMap);
-
-        // append comments
-        chunk
-          .filter(doc => idsHavingComment.includes(doc._id.toString()))
-          .forEach((doc) => {
-            // append comments from idToCommentMap
-            doc.comments = idToCommentMap[doc._id.toString()];
-          });
-
-        this.push(chunk);
-        callback();
-      },
-    });
+    const bulkSize: number = configManager.getConfig('crowi', 'app:elasticsearchReindexBulkSize');
+    const batchStream = createBatchStream(bulkSize);
 
     const appendTagNamesStream = new Transform({
       objectMode: true,
@@ -552,7 +469,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
         // append tagNames
         chunk
           .filter(doc => idsHavingTagNames.includes(doc._id.toString()))
-          .forEach((doc) => {
+          .forEach((doc: AggregatedPage) => {
             // append tagName from idToTagNamesMap
             doc.tagNames = idToTagNamesMap[doc._id.toString()];
           });
@@ -566,8 +483,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     const writeStream = new Writable({
       objectMode: true,
       async write(batch, encoding, callback) {
-        const body = [];
-        batch.forEach(doc => prepareBodyForCreate(body, doc));
+        const body: (BulkWriteCommand|BulkWriteBody)[] = [];
+        batch.forEach((doc: AggregatedPage) => {
+          body.push(...prepareBodyForCreate(doc));
+        });
 
         try {
           const bulkResponse = await bulkWrite({
@@ -580,7 +499,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
           logger.info(`Adding pages progressing: (count=${count}, errors=${bulkResponse.errors}, took=${bulkResponse.took}ms)`);
 
           if (shouldEmitProgress) {
-            socket?.emit(SocketEventName.AddPageProgress, { totalCount, count, skipped });
+            socket?.emit(SocketEventName.AddPageProgress, { totalCount, count });
           }
         }
         catch (err) {
@@ -601,20 +520,17 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
         callback();
       },
       final(callback) {
-        logger.info(`Adding pages has completed: (totalCount=${totalCount}, skipped=${skipped})`);
+        logger.info(`Adding pages has completed: (totalCount=${totalCount})`);
 
         if (shouldEmitProgress) {
-          socket?.emit(SocketEventName.FinishAddPage, { totalCount, count, skipped });
+          socket?.emit(SocketEventName.FinishAddPage, { totalCount, count });
         }
         callback();
       },
     });
 
     readStream
-      .pipe(thinOutStream)
       .pipe(batchStream)
-      .pipe(appendBookmarkCountStream)
-      .pipe(appendCommentStream)
       .pipe(appendTagNamesStream)
       .pipe(writeStream);
 
@@ -829,8 +745,8 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
 
   async filterPagesByViewer(query, user, userGroups) {
-    const showPagesRestrictedByOwner = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
-    const showPagesRestrictedByGroup = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
+    const showPagesRestrictedByOwner = !configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const showPagesRestrictedByGroup = !configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
 
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
@@ -977,30 +893,12 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   async syncPageUpdated(page, user) {
     logger.debug('SearchClient.syncPageUpdated', page.path);
-
-    // delete if page should not indexed
-    if (!this.shouldIndexed(page)) {
-      try {
-        await this.deletePages([page]);
-      }
-      catch (err) {
-        logger.error('deletePages:ES Error', err);
-      }
-      return;
-    }
-
     return this.updateOrInsertPageById(page._id);
   }
 
   // remove pages whitch should nod Indexed
   async syncPagesUpdated(pages, user) {
     const shoudDeletePages: any[] = [];
-    pages.forEach((page) => {
-      logger.debug('SearchClient.syncPageUpdated', page.path);
-      if (!this.shouldIndexed(page)) {
-        shoudDeletePages.push(page);
-      }
-    });
 
     // delete if page should not indexed
     try {

+ 7 - 9
apps/app/src/server/service/search.ts

@@ -4,19 +4,20 @@ import { FilterXSS } from 'xss';
 
 import { CommentEvent, commentEvent } from '~/features/comment/server';
 import { SearchDelegatorName } from '~/interfaces/named-query';
-import { IFormattedSearchResult, IPageWithSearchMeta, ISearchResult } from '~/interfaces/search';
+import type { IFormattedSearchResult, IPageWithSearchMeta, ISearchResult } from '~/interfaces/search';
 import loggerFactory from '~/utils/logger';
 
-import { ObjectIdLike } from '../interfaces/mongoose-utils';
-import {
+import type { ObjectIdLike } from '../interfaces/mongoose-utils';
+import type {
   SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, SearchableData, QueryTerms,
 } from '../interfaces/search';
 import NamedQuery from '../models/named-query';
-import { PageModel } from '../models/page';
+import type { PageModel } from '../models/page';
 import { serializeUserSecurely } from '../models/serializers/user-serializer';
 import { SearchError } from '../models/vo/search-error';
 import { hasIntersection } from '../util/compare-objectId';
 
+import { configManager } from './config-manager';
 import ElasticsearchDelegator from './search-delegator/elasticsearch';
 import PrivateLegacyPagesDelegator from './search-delegator/private-legacy-pages';
 
@@ -76,8 +77,6 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
   crowi!: any;
 
-  configManager!: any;
-
   isErrorOccuredOnHealthcheck: boolean | null;
 
   isErrorOccuredOnSearching: boolean | null;
@@ -88,7 +87,6 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
   constructor(crowi) {
     this.crowi = crowi;
-    this.configManager = crowi.configManager;
 
     this.isErrorOccuredOnHealthcheck = null;
     this.isErrorOccuredOnSearching = null;
@@ -117,7 +115,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
   }
 
   get isElasticsearchEnabled() {
-    const uri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
+    const uri = configManager.getConfig('crowi', 'app:elasticsearchUri');
     return uri != null && uri.length > 0;
   }
 
@@ -126,7 +124,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
     if (this.isElasticsearchEnabled) {
       logger.info('Elasticsearch is enabled');
-      return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
+      return new ElasticsearchDelegator(this.crowi.socketIoService);
     }
 
     logger.info('No elasticsearch URI is specified so that full text search is disabled.');

+ 2 - 1
apps/app/src/server/service/slack-command-handler/keep.js

@@ -1,11 +1,12 @@
 import {
   inputBlock, actionsBlock, buttonElement, markdownSectionBlock,
 } from '@growi/slack/dist/utils/block-kit-builder';
+import { format } from 'date-fns/format';
+import { parse } from 'date-fns/parse';
 
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:service:SlackBotService:keep');
-const { parse, format } = require('date-fns');
 
 const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 

+ 3 - 2
apps/app/src/server/service/slack-command-handler/togetter.js

@@ -2,13 +2,14 @@ import {
   inputBlock, actionsBlock, buttonElement, markdownSectionBlock, divider,
 } from '@growi/slack/dist/utils/block-kit-builder';
 import { respond, deleteOriginal } from '@growi/slack/dist/utils/response-url';
+import { format, formatDate } from 'date-fns/format';
+import { parse } from 'date-fns/parse';
 
 import loggerFactory from '~/utils/logger';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:SlackBotService:togetter');
 
-const { parse, format } = require('date-fns');
 
 const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
@@ -163,7 +164,7 @@ module.exports = (crowi) => {
         // include header
         else {
           const ts = (parseInt(message.ts) - grwTzoffset) * 1000;
-          const time = format(new Date(ts), 'h:mm a');
+          const time = formatDate(new Date(ts), 'h:mm a');
           cleanedContents.push(`${message.user}  ${time}\n${message.text}\n`);
           lastMessage = message;
         }

+ 4 - 0
apps/app/src/stores-universal/context.tsx

@@ -88,6 +88,10 @@ export const useIsSearchServiceReachable = (initialData?: boolean) : SWRResponse
   return useContextSWR<boolean, Error>('isSearchServiceReachable', initialData);
 };
 
+export const useElasticsearchMaxBodyLengthToIndex = (initialData?: number) : SWRResponse<number, Error> => {
+  return useContextSWR('elasticsearchMaxBodyLengthToIndex', initialData);
+};
+
 export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, any> => {
   return useContextSWR('isMailerSetup', initialData);
 };

+ 2 - 1
apps/app/src/stores/admin/customize.tsx

@@ -1,6 +1,7 @@
 import { useCallback } from 'react';
 
-import useSWR, { SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+import useSWR from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';

+ 0 - 219
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

@@ -1,219 +0,0 @@
-context('Modal for page operation', () => {
-
-  const ssPrefix = 'modal-for-page-operation-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('Page Deletion and PutBack is executed successfully', { scrollBehavior: false }, () => {
-    cy.visit('/Sandbox/Bootstrap5');
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      //wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-delete-modal-btn').filter(':visible').click({force: true});
-
-    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}-delete-modal`);
-      cy.getByTestid('delete-page-button').click();
-    });
-
-    cy.getByTestid('trash-page-alert').should('be.visible');
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-bootstrap5-is-in-garbage-box`);
-
-    cy.getByTestid('put-back-button').click();
-    cy.getByTestid('put-back-page-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}-put-back-modal`);
-      cy.getByTestid('put-back-execution-button').should('be.visible').click();
-    });
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-put-backed-bootstrap5-page`);
-  });
-
-  it('PageDuplicateModal is shown successfully', () => {
-    cy.visit('/Sandbox/5');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-duplicate-modal-btn').filter(':visible').click({force: true});
-
-    cy.getByTestid('page-duplicate-modal').should('be.visible').screenshot(`${ssPrefix}-duplicate-bootstrap5`);
-  });
-
-  it('PageMoveRenameModal is shown successfully', () => {
-    cy.visit('/Sandbox/Bootstrap5');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('rename-page-btn').filter(':visible').click({force: true});
-    cy.getByTestid('grw-page-rename-button').should('be.disabled');
-
-    cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap5`);
-  });
-
-});
-
-
-// TODO: Uncomment after https://redmine.weseek.co.jp/issues/103121 is resolved
-// context('Open presentation modal', () => {
-
-//   const ssPrefix = 'access-to-presentation-modal-';
-
-//   beforeEach(() => {
-//     // login
-//     cy.fixture("user-admin.json").then(user => {
-//       cy.login(user.username, user.password);
-//     });
-//     cy.collapseSidebar(true);
-//   });
-
-//   it('PresentationModal for "/" is shown successfully', () => {
-//     cy.visit('/');
-
-//     cy.getByTestid('grw-contextual-sub-nav').within(() => {
-//       cy.getByTestid('open-page-item-control-btn').click({force: true});
-//       cy.getByTestid('open-presentation-modal-btn').click({force: true});
-//     });
-
-//     // eslint-disable-next-line cypress/no-unnecessary-waiting
-//     cy.wait(1500);
-//     cy.screenshot(`${ssPrefix}-open-top`);
-//   });
-
-// });
-
-context('Page Accessories Modal', () => {
-
-  const ssPrefix = 'access-to-page-accessories-modal';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-
-    cy.visit('/');
-    cy.collapseSidebar(true, true);
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      //wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-
-  });
-
-  it('Page History is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-history-tab').click({force: true});
-    });
-
-    cy.getByTestid('page-history').should('be.visible');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.screenshot(`${ssPrefix}-open-page-history-bootstrap5`);
-  });
-
-  it('Page Attachment Data is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-attachment-data-tab').click({force: true});
-    });
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
-
-    cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap5`);
-  });
-
-  it('Share Link Management is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').click({force: true});
-    });
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('page-accessories-modal').should('be.visible');
-    cy.getByTestid('share-link-management').should('be.visible');
-
-    cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap5`);
-  });
-});
-
-context('Tag Oprations', { scrollBehavior: false }, () =>{
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('Successfully add new tag', () => {
-    const ssPrefix = 'tag-operations-add-new-tag-'
-    const tag = 'we';
-
-    cy.visit('/Sandbox/Bootstrap5');
-    cy.collapseSidebar(true);
-
-    // Add tag
-    cy.get('#edit-tags-btn-wrapper-for-tooltip').as('edit-tag-tooltip').should('be.visible');
-
-    // open Edit Tags Modal
-    cy.waitUntil(() => {
-      // do
-      cy.get('@edit-tag-tooltip').find('a').click({force: true});
-      // wait until
-      return cy.get('#edit-tag-modal').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#edit-tag-modal').should('be.visible').screenshot(`${ssPrefix}1-edit-tag-input`);
-
-    cy.get('#edit-tag-modal').should('be.visible').within(() => {
-      cy.get('.rbt-input-main').type(tag);
-      cy.get('#tag-typeahead-asynctypeahead').should('be.visible');
-      cy.get('#tag-typeahead-asynctypeahead-item-0').should('be.visible');
-      // select
-      cy.get('a#tag-typeahead-asynctypeahead-item-0').click();
-      // save
-      cy.get('div.modal-footer > button').click();
-    });
-
-    cy.get('.Toastify__toast').should('be.visible').trigger('mouseover');
-    cy.getByTestid('grw-tag-labels').contains(tag).should('exist');
-
-    cy.screenshot(`${ssPrefix}2-click-done`);
-  });
-
-});

+ 2 - 2
packages/core-styles/scss/variables/_growi-official-colors.scss

@@ -1,3 +1,3 @@
 // == GROWI Official Color
-$growi-green: #74bc46;
-$growi-blue: #175fa5;
+$growi-green: #7AD340;
+$growi-blue: #428DD1;

+ 3 - 1
packages/editor/package.json

@@ -33,6 +33,8 @@
     "@codemirror/merge": "6.0.0",
     "@codemirror/state": "^6.2.1",
     "@codemirror/view": "^6.15.3",
+    "@emoji-mart/data": "^1.2.1",
+    "@emoji-mart/react": "^1.1.1",
     "@growi/core": "link:../core",
     "@growi/core-styles": "link:../core-styles",
     "@popperjs/core": "^2.11.8",
@@ -51,7 +53,7 @@
     "cm6-theme-nord": "^0.2.0",
     "codemirror": "^6.0.1",
     "csv-to-markdown-table": "^1.4.1",
-    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
+    "emoji-mart": "^5.6.0",
     "eslint-plugin-react-refresh": "^0.4.1",
     "markdown-table": "^3.0.3",
     "react-dropzone": "^14.2.3",

+ 2 - 0
packages/editor/src/@types/emoji-mart.d.ts

@@ -1 +1,3 @@
 declare module 'emoji-mart';
+declare module '@emoji-mart/data';
+declare module '@emoji-mart/react';

+ 23 - 79
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EmojiButton.tsx

@@ -1,13 +1,12 @@
 import {
   useState, useCallback,
-  type FC, type CSSProperties,
+  type CSSProperties,
 } from 'react';
 
-import { Picker } from 'emoji-mart';
-import i18n from 'i18next';
+import emojiData from '@emoji-mart/data';
+import Picker from '@emoji-mart/react';
 import { Modal } from 'reactstrap';
 
-import 'emoji-mart/css/emoji-mart.css';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
 import { useResolvedThemeForEditor } from '../../../stores/use-resolved-theme';
 
@@ -15,88 +14,32 @@ type Props = {
   editorKey: string,
 }
 
-type Translation = {
-  search: string
-  clear: string
-  notfound: string
-  skintext: string
-  categories: object
-  categorieslabel: string
-  skintones: object
-  title: string
-}
-
-// TODO: https://redmine.weseek.co.jp/issues/133681
-const getEmojiTranslation = (): Translation => {
-
-  const categories: { [key: string]: string } = {};
-  [
-    'search',
-    'recent',
-    'smileys',
-    'people',
-    'nature',
-    'foods',
-    'activity',
-    'places',
-    'objects',
-    'symbols',
-    'flags',
-    'custom',
-  ].forEach((category) => {
-    categories[category] = i18n.t(`emoji.categories.${category}`);
-  });
-
-  const skintones: { [key: string]: string} = {};
-  (Array.from(Array(6).keys())).forEach((tone) => {
-    skintones[tone + 1] = i18n.t(`emoji.skintones.${tone + 1}`);
-  });
-
-  const translation = {
-    search: i18n.t('emoji.search'),
-    clear: i18n.t('emoji.clear'),
-    notfound: i18n.t('emoji.notfound'),
-    skintext: i18n.t('emoji.skintext'),
-    categories,
-    categorieslabel: i18n.t('emoji.categorieslabel'),
-    skintones,
-    title: i18n.t('emoji.title'),
-  };
-
-  return translation;
-};
-
-const translation = getEmojiTranslation();
-
-export const EmojiButton: FC<Props> = (props) => {
+export const EmojiButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
 
   const [isOpen, setIsOpen] = useState(false);
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
   const { data: resolvedTheme } = useResolvedThemeForEditor();
-
-  const view = codeMirrorEditor?.view;
-  const cursorIndex = view?.state.selection.main.head;
   const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
 
-  const selectEmoji = useCallback((emoji: { colons: string }): void => {
+  const selectEmoji = useCallback((emoji: { shortcodes: string }): void => {
 
-    if (cursorIndex == null || !isOpen) {
+    if (!isOpen) {
       return;
     }
 
-    view?.dispatch({
-      changes: {
-        from: cursorIndex,
-        insert: emoji.colons,
-      },
-    });
+    codeMirrorEditor?.insertText(emoji.shortcodes);
 
     toggle();
-  }, [cursorIndex, isOpen, toggle, view]);
+  }, [isOpen, toggle, codeMirrorEditor]);
+
 
   const setStyle = useCallback((): CSSProperties => {
+
+    const view = codeMirrorEditor?.view;
+    const cursorIndex = view?.state.selection.main.head;
+
     if (view == null || cursorIndex == null || !isOpen) {
       return {};
     }
@@ -123,7 +66,7 @@ export const EmojiButton: FC<Props> = (props) => {
       left: cursorRect.left + offset,
       position: 'fixed',
     };
-  }, [cursorIndex, isOpen, view]);
+  }, [isOpen, codeMirrorEditor]);
 
   return (
     <>
@@ -134,14 +77,15 @@ export const EmojiButton: FC<Props> = (props) => {
       && (
         <div className="mb-2 d-none d-md-block">
           <Modal isOpen={isOpen} toggle={toggle} backdropClassName="emoji-picker-modal" fade={false}>
-            <Picker
-              onSelect={selectEmoji}
-              i18n={translation}
-              title={translation.title}
-              emojiTooltip
-              style={setStyle()}
-              theme={resolvedTheme}
-            />
+            <span style={setStyle()}>
+              <Picker
+                onEmojiSelect={selectEmoji}
+                theme={resolvedTheme?.themeData}
+                data={emojiData}
+                // TODO: https://redmine.weseek.co.jp/issues/133681
+                // i18n={}
+              />
+            </span>
           </Modal>
         </div>
       )}

+ 4 - 6
packages/editor/src/client/services-internal/extensions/emojiAutocompletionSettings.ts

@@ -1,7 +1,7 @@
 import { type CompletionContext, type Completion, autocompletion } from '@codemirror/autocomplete';
 import { syntaxTree } from '@codemirror/language';
-import { emojiIndex } from 'emoji-mart';
-import emojiData from 'emoji-mart/data/all.json';
+import emojiData from '@emoji-mart/data';
+
 
 const getEmojiDataArray = (): string[] => {
   const rawEmojiDataArray = emojiData.categories;
@@ -20,7 +20,7 @@ const getEmojiDataArray = (): string[] => {
   const fixedEmojiDataArray: string[] = [];
 
   emojiCategoriesData.forEach((value) => {
-    const tempArray = rawEmojiDataArray.find(obj => obj.id === value)?.emojis;
+    const tempArray = rawEmojiDataArray.find((obj: {id: string}) => obj.id === value)?.emojis;
 
     if (tempArray == null) {
       return;
@@ -60,9 +60,7 @@ export const emojiAutocompletionSettings = autocompletion({
   addToOptions: [{
     render: (completion: Completion) => {
       const emojiName = completion.type ?? '';
-      const emojiData = emojiIndex.emojis[emojiName];
-
-      const emoji = emojiData.native ?? emojiData[1].native;
+      const emoji = emojiData.emojis[emojiName].skins[0].native;
 
       const element = document.createElement('span');
       element.innerHTML = emoji;

+ 0 - 3
packages/editor/vite.config.ts

@@ -50,9 +50,6 @@ export default defineConfig({
         preserveModules: true,
         preserveModulesRoot: 'src',
       },
-      external: [
-        'emoji-mart/css/emoji-mart.css',
-      ],
     },
   },
 });

+ 3 - 0
packages/presentation/package.json

@@ -18,6 +18,9 @@
     "./dist/client": {
       "import": "./dist/client/index.js"
     },
+    "./dist/client/services/sanitize-option": {
+      "import": "./dist/client/services/sanitize-option.js"
+    },
     "./dist/services": {
       "import": "./dist/services/index.js"
     },

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

@@ -1,13 +0,0 @@
-:root[data-bs-theme='light'] {
-  .reveal-viewport {
-    // adjust marp default theme
-    background-color: #fff;
-  }
-}
-
-:root[data-bs-theme='dark'] {
-  .reveal-viewport {
-    // adjust marp default theme
-    background-color: #0d1117;
-  }
-}

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

@@ -1,3 +1,10 @@
+:root {
+  :global {
+    /* stylelint-disable-next-line no-invalid-position-at-import-rule */
+    @import 'reveal.js/dist/reveal.css';
+  }
+}
+
 .grw-presentation {
   // workaround from https://github.com/css-modules/css-modules/issues/295#issuecomment-952885628
   &:global(.reveal) :global {
@@ -19,3 +26,24 @@
   }
 
 }
+
+// adjust marp default theme
+:root[data-bs-theme='light'] {
+  .grw-presentation {
+    &:global {
+      &.reveal-viewport {
+        background-color: #fff;
+      }
+    }
+  }
+}
+
+:root[data-bs-theme='dark'] {
+  .grw-presentation {
+    &:global {
+      &.reveal-viewport {
+        background-color: #0d1117;
+      }
+    }
+  }
+}

+ 4 - 5
packages/presentation/src/client/components/Presentation.tsx

@@ -6,11 +6,10 @@ import type { PresentationOptions } from '../consts';
 
 import { Slides } from './Slides';
 
-import 'reveal.js/dist/reveal.css';
-import './Presentation.global.scss';
-
 import styles from './Presentation.module.scss';
 
+const moduleClass = styles['grw-presentation'] ?? '';
+
 
 const baseRevealOptions: Reveal.Options = {
   // adjust size to the marp preset size
@@ -27,7 +26,7 @@ const baseRevealOptions: Reveal.Options = {
  * @see https://getbootstrap.com/docs/4.6/content/reboot/#html5-hidden-attribute
  */
 const removeAllHiddenElements = () => {
-  const sections = document.querySelectorAll('.grw-presentation section');
+  const sections = document.querySelectorAll(`${moduleClass} section`);
   sections.forEach(section => section.removeAttribute('hidden'));
 };
 
@@ -59,7 +58,7 @@ export const Presentation = (props: PresentationProps): JSX.Element => {
   }, [children, revealOptions]);
 
   return (
-    <div className={`grw-presentation ${styles['grw-presentation']} reveal`}>
+    <div className={`${moduleClass} reveal`}>
       <Slides options={options} hasMarpFlag={marp} presentation>{children}</Slides>
     </div>
   );

+ 2 - 1
packages/presentation/src/client/services/growi-marpit.ts

@@ -1,4 +1,5 @@
-import { Marp, MarpOptions } from '@marp-team/marp-core';
+import type { MarpOptions } from '@marp-team/marp-core';
+import { Marp } from '@marp-team/marp-core';
 import { Element } from '@marp-team/marpit';
 
 export const MARP_CONTAINER_CLASS_NAME = 'marpit';

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

@@ -83,5 +83,5 @@ export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) =>
 
 
 export const sanitizeOption: SanitizeOption = {
-  // tagNames: ['slides', 'slide'],
+  tagNames: ['section'],
 };

+ 1 - 0
packages/presentation/src/client/services/sanitize-option.ts

@@ -0,0 +1 @@
+export { sanitizeOption } from './renderer/extract-sections';

+ 56 - 42
yarn.lock

@@ -747,6 +747,13 @@
   dependencies:
     tslib "^2.2.0"
 
+"@azure/abort-controller@^2.0.0":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d"
+  integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==
+  dependencies:
+    tslib "^2.6.2"
+
 "@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44"
@@ -756,18 +763,18 @@
     "@azure/core-util" "^1.1.0"
     tslib "^2.2.0"
 
-"@azure/core-client@^1.4.0":
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.7.3.tgz#f8cb2a1f91e8bc4921fa2e745cfdfda3e6e491a3"
-  integrity sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==
+"@azure/core-client@^1.9.2":
+  version "1.9.2"
+  resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74"
+  integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==
   dependencies:
-    "@azure/abort-controller" "^1.0.0"
+    "@azure/abort-controller" "^2.0.0"
     "@azure/core-auth" "^1.4.0"
     "@azure/core-rest-pipeline" "^1.9.1"
     "@azure/core-tracing" "^1.0.0"
-    "@azure/core-util" "^1.0.0"
+    "@azure/core-util" "^1.6.1"
     "@azure/logger" "^1.0.0"
-    tslib "^2.2.0"
+    tslib "^2.6.2"
 
 "@azure/core-http@^3.0.0":
   version "3.0.3"
@@ -836,28 +843,28 @@
   dependencies:
     tslib "^2.2.0"
 
-"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0":
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.6.1.tgz#fea221c4fa43c26543bccf799beb30c1c7878f5a"
-  integrity sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ==
+"@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1":
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.0.tgz#469afd7e6452d5388b189f90d33f7756b0b210d1"
+  integrity sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==
   dependencies:
-    "@azure/abort-controller" "^1.0.0"
-    tslib "^2.2.0"
+    "@azure/abort-controller" "^2.0.0"
+    tslib "^2.6.2"
 
-"@azure/identity@^4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.0.1.tgz#16a885d384fd06447a21da92c08960df492fe91e"
-  integrity sha512-yRdgF03SFLqUMZZ1gKWt0cs0fvrDIkq2bJ6Oidqcoo5uM85YMBnXWMzYKK30XqIT76lkFyAaoAAy5knXhrG4Lw==
+"@azure/identity@^4.3.0":
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.3.0.tgz#e8da6b3bf1df4de1511e813a7166a4b5b4a99ca1"
+  integrity sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==
   dependencies:
     "@azure/abort-controller" "^1.0.0"
     "@azure/core-auth" "^1.5.0"
-    "@azure/core-client" "^1.4.0"
+    "@azure/core-client" "^1.9.2"
     "@azure/core-rest-pipeline" "^1.1.0"
     "@azure/core-tracing" "^1.0.0"
     "@azure/core-util" "^1.3.0"
     "@azure/logger" "^1.0.0"
-    "@azure/msal-browser" "^3.5.0"
-    "@azure/msal-node" "^2.5.1"
+    "@azure/msal-browser" "^3.11.1"
+    "@azure/msal-node" "^2.9.2"
     events "^3.0.0"
     jws "^4.0.0"
     open "^8.0.0"
@@ -871,24 +878,24 @@
   dependencies:
     tslib "^2.2.0"
 
-"@azure/msal-browser@^3.5.0":
-  version "3.10.0"
-  resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.10.0.tgz#8925659e8d1a4bd21e389cca4683eb52658c778e"
-  integrity sha512-mnmi8dCXVNZI+AGRq0jKQ3YiodlIC4W9npr6FCB9WN6NQT+6rq+cIlxgUb//BjLyzKsnYo+i4LROGeMyU+6v1A==
+"@azure/msal-browser@^3.11.1":
+  version "3.17.0"
+  resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.17.0.tgz#dee9ccae586239e7e0708b261f7ffa5bc7e00fb7"
+  integrity sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==
   dependencies:
-    "@azure/msal-common" "14.7.1"
+    "@azure/msal-common" "14.12.0"
 
-"@azure/msal-common@14.7.1":
-  version "14.7.1"
-  resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.7.1.tgz#b13443fbacc87ce2019a91e81a6582ea73847c75"
-  integrity sha512-v96btzjM7KrAu4NSEdOkhQSTGOuNUIIsUdB8wlyB9cdgl5KqEKnTonHUZ8+khvZ6Ap542FCErbnTyDWl8lZ2rA==
+"@azure/msal-common@14.12.0":
+  version "14.12.0"
+  resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.12.0.tgz#844abe269b071f8fa8949dadc2a7b65bbb147588"
+  integrity sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==
 
-"@azure/msal-node@^2.5.1":
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.6.4.tgz#457bd86a52461178ab2d1ba3d9d6705d95b2186e"
-  integrity sha512-nNvEPx009/80UATCToF+29NZYocn01uKrB91xtFr7bSqkqO1PuQGXRyYwryWRztUrYZ1YsSbw9A+LmwOhpVvcg==
+"@azure/msal-node@^2.9.2":
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.9.2.tgz#e6d3c1661012c1bd0ef68e328f73a2fdede52931"
+  integrity sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==
   dependencies:
-    "@azure/msal-common" "14.7.1"
+    "@azure/msal-common" "14.12.0"
     jsonwebtoken "^9.0.0"
     uuid "^8.3.0"
 
@@ -1176,7 +1183,7 @@
     core-js-pure "^3.20.2"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.1", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.1", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
   integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
@@ -1889,6 +1896,16 @@
   dependencies:
     tslib "^2.4.0"
 
+"@emoji-mart/data@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c"
+  integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==
+
+"@emoji-mart/react@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a"
+  integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==
+
 "@esbuild/aix-ppc64@0.20.2":
   version "0.20.2"
   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
@@ -8193,13 +8210,10 @@ emittery@^0.13.1:
   resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
   integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
 
-"emoji-mart@npm:panta82-emoji-mart@^3.0.1":
-  version "3.0.1003"
-  resolved "https://registry.yarnpkg.com/panta82-emoji-mart/-/panta82-emoji-mart-3.0.1003.tgz#8febed01a0a731ba84caaddf1ba5b1ac724562ac"
-  integrity sha512-JLCNrxoyOb/m/0kGWJZK7QGl/+t82cQrFgbbieeevBxp+lD8pnAb4Bsa4kJzV7xNwMYlNlHDAZJsM//Xb5eJ2Q==
-  dependencies:
-    "@babel/runtime" "^7.0.0"
-    prop-types "^15.6.0"
+emoji-mart@^5.6.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
+  integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
 
 emoji-regex@^8.0.0:
   version "8.0.0"