Przeglądaj źródła

Merge branch 'support/136168-update-axios' into support/136168-136169-update-axios-for-pdf-converter-client-and-remark-packages

Futa Arai 7 miesięcy temu
rodzic
commit
bc9bf6932a
80 zmienionych plików z 1734 dodań i 522 usunięć
  1. 2 0
      apps/app/.eslintrc.js
  2. 11 0
      apps/app/public/static/locales/en_US/translation.json
  3. 11 0
      apps/app/public/static/locales/fr_FR/translation.json
  4. 11 0
      apps/app/public/static/locales/ja_JP/translation.json
  5. 11 0
      apps/app/public/static/locales/zh_CN/translation.json
  6. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx
  7. 31 30
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx
  8. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx
  9. 28 7
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHeader.tsx
  10. 23 11
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx
  11. 100 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.module.scss
  12. 228 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.tsx
  13. 49 23
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx
  14. 35 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageSelectionMethod.tsx
  15. 11 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.module.scss
  16. 171 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx
  17. 28 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.module.scss
  18. 55 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.tsx
  19. 43 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePageList.module.scss
  20. 249 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePageList.tsx
  21. 0 43
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectedPageList.tsx
  22. 4 4
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx
  23. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx
  24. 72 0
      apps/app/src/features/openai/client/services/use-selected-pages.tsx
  25. 17 2
      apps/app/src/features/openai/client/stores/ai-assistant.tsx
  26. 10 0
      apps/app/src/features/openai/interfaces/selectable-page.ts
  27. 0 6
      apps/app/src/features/openai/interfaces/selected-page.ts
  28. 2 7
      apps/app/src/features/openai/server/routes/middlewares/upsert-ai-assistant-validator.ts
  29. 13 0
      apps/app/src/features/openai/utils/is-creatable-page-path-pattern.ts
  30. 3 3
      apps/app/src/linter-checker/test.js
  31. 3 2
      apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js
  32. 3 2
      apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js
  33. 0 3
      apps/app/src/migrations/20180926134048-make-email-unique.js
  34. 13 9
      apps/app/src/migrations/20180927102719-init-serverurl.js
  35. 12 7
      apps/app/src/migrations/20181019114028-abolish-page-group-relation.js
  36. 3 2
      apps/app/src/migrations/20190618104011-add-config-app-installed.js
  37. 4 7
      apps/app/src/migrations/20190619055421-adjust-page-grant.js
  38. 0 2
      apps/app/src/migrations/20190624110950-fill-last-update-user.js
  39. 3 1
      apps/app/src/migrations/20191102223901-drop-pages-indices.js
  40. 0 2
      apps/app/src/migrations/20191126173016-adjust-pages-path.js
  41. 6 2
      apps/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  42. 6 4
      apps/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js
  43. 8 2
      apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  44. 2 8
      apps/app/src/migrations/20200620203632-normalize-locale-id.js
  45. 1 1
      apps/app/src/migrations/20200827045151-remove-layout-setting.js
  46. 3 1
      apps/app/src/migrations/20200828024025-copy-aws-setting.js
  47. 10 6
      apps/app/src/migrations/20200901034313-update-mail-transmission.js
  48. 0 1
      apps/app/src/migrations/20200903080025-remove-timeline-type.js.js
  49. 2 3
      apps/app/src/migrations/20200915035234-rename-s3-config.js
  50. 5 2
      apps/app/src/migrations/20210420160380-convert-double-to-date.js
  51. 8 4
      apps/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js
  52. 7 3
      apps/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js
  53. 22 19
      apps/app/src/migrations/20210921173042-add-is-trashed-field.js
  54. 12 5
      apps/app/src/migrations/20211005120030-slack-app-integration-rename-keys.js
  55. 22 7
      apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js
  56. 19 10
      apps/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js
  57. 19 16
      apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js
  58. 16 8
      apps/app/src/migrations/20220131001218-convert-redirect-to-pages-to-page-redirect-documents.js
  59. 21 24
      apps/app/src/migrations/20220311011114-convert-page-delete-config.js
  60. 3 1
      apps/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js
  61. 15 13
      apps/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js
  62. 3 2
      apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  63. 3 2
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  64. 9 4
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  65. 51 42
      apps/app/src/migrations/20230723061824-granted-group-to-array-of-objects.js
  66. 18 11
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  67. 0 1
      apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js
  68. 6 9
      apps/app/src/migrations/20231223155127-non-null-granted-groups.js
  69. 9 10
      apps/app/src/migrations/20240924181317-changed-status-in-inappnotifications-from-unread-to-unopened.js
  70. 19 12
      apps/app/src/migrations/20241107172359-rename-pageId-to-page.js
  71. 30 9
      apps/app/src/migrations/20250522105040-delete-old-index-for-vector-store-file-relation.js
  72. 4 4
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  73. 0 0
      apps/app/src/server/service/search-delegator/mappings/mappings-es7.ts
  74. 0 0
      apps/app/src/server/service/search-delegator/mappings/mappings-es8.ts
  75. 0 0
      apps/app/src/server/service/search-delegator/mappings/mappings-es9-for-ci.ts
  76. 0 0
      apps/app/src/server/service/search-delegator/mappings/mappings-es9.ts
  77. 1 1
      apps/slackbot-proxy/package.json
  78. 12 1
      biome.json
  79. 1 1
      packages/slack/package.json
  80. 99 97
      pnpm-lock.yaml

+ 2 - 0
apps/app/.eslintrc.js

@@ -28,6 +28,8 @@ module.exports = {
     'test/integration/setup.js',
     'bin/**',
     'config/**',
+    'src/linter-checker/**',
+    'src/migrations/**',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript

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

@@ -600,6 +600,17 @@
       "create_failed": "Failed to create assistant",
       "update_failed": "Failed to update assistant"
     },
+    "select_source_pages": "Select pages for the assistant to reference",
+    "search_reference_pages_by_keyword": "Search for pages the assistant will reference by keyword",
+    "search_by_keyword": "Search by keyword",
+    "enter_keywords": "Enter keywords",
+    "max_items_space_separated_hint": "Enter up to 5 items separated by spaces",
+    "select_assistant_reference_pages": "Select pages for the assistant to reference",
+    "reference_pages": "Reference pages",
+    "no_pages_selected": "No pages selected",
+    "can_add_later": "You can add more later",
+    "next": "Next",
+    "select_from_page_tree": "Select from page tree",
     "edit_page_description": "Edit pages that the assistant can reference.<br> The assistant can reference up to {{limitLearnablePageCountPerAssistant}} pages including child pages.",
     "default_instruction": "You are the knowledge assistant for this Wiki.\n\n## Multilingual Support:\nRespond in the same language the user uses in their input.\n",
     "add_page_button": "Add page",

+ 11 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -594,6 +594,17 @@
       "create_failed": "Échec de la création de l'assistant",
       "update_failed": "Échec de la mise à jour de l'assistant"
     },
+    "select_source_pages": "Sélectionnez les pages que l'assistant doit référencer",
+    "search_reference_pages_by_keyword": "Rechercher les pages de référence de l'assistant par mot-clé",
+    "search_by_keyword": "Rechercher par mot-clé",
+    "max_items_space_separated_hint": "Saisissez jusqu'à 5 éléments séparés par des espaces",
+    "select_assistant_reference_pages": "Sélectionnez les pages de référence pour l'assistant",
+    "enter_keywords": "Entrer des mots-clés",
+    "reference_pages": "Pages de référence",
+    "no_pages_selected": "Aucune page sélectionnée",
+    "can_add_later": "Vous pouvez en ajouter plus tard",
+    "next": "Suivant",
+    "select_from_page_tree": "Sélectionner depuis l'arborescence des pages",
     "edit_page_description": "Modifier les pages que l'assistant peut référencer.<br> L'assistant peut référencer jusqu'à {{limitLearnablePageCountPerAssistant}} pages, y compris les pages enfants.",
     "default_instruction": "Vous êtes l'assistant de connaissances pour ce Wiki.\n\n## Support multilingue :\nRépondez dans la même langue que celle utilisée par l'utilisateur dans sa requête.\n",
     "add_page_button": "Ajouter une page",

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

@@ -633,6 +633,17 @@
       "create_failed": "アシスタントの作成に失敗しました",
       "update_failed": "アシスタントの更新に失敗しました"
     },
+    "select_source_pages": "アシスタントが参照するページを選択します",
+    "search_reference_pages_by_keyword": "アシスタントが参照するページをキーワードで検索",
+    "search_by_keyword": "キーワードで検索",
+    "enter_keywords": "キーワードを入力",
+    "max_items_space_separated_hint": "スペース区切りで最大5つまで入力できます",
+    "select_assistant_reference_pages": "アシスタントが参照するページを選択してください",
+    "reference_pages": "参照するページ",
+    "no_pages_selected": "ページが選択されていません",
+    "can_add_later": "あとからでも追加できます",
+    "next": "次へ",
+    "select_from_page_tree": "ページツリーから選択",
     "edit_page_description": " アシスタントが参照するページを編集します。<br> 参照できるページは配下ページも含めて {{limitLearnablePageCountPerAssistant}} ページまでです。",
     "default_instruction": "あなたはこのWikiの知識アシスタントです。\n\n## 多言語サポート:\nユーザーが入力で使用した言語と同じ言語で応答してください。\n",
     "add_page_button": "ページを追加する",

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

@@ -591,6 +591,17 @@
       "create_failed": "创建助手失败",
       "update_failed": "更新助手失败"
     },
+    "select_source_pages": "选择助手要参考的页面",
+    "search_reference_pages_by_keyword": "按关键词搜索助手参考的页面",
+    "search_by_keyword": "按关键词搜索",
+    "max_items_space_separated_hint": "请输入最多5个项目,用空格分隔",
+    "select_assistant_reference_pages": "请选择助手参考的页面",
+    "enter_keywords": "输入关键词",
+    "reference_pages": "参考页面",
+    "no_pages_selected": "未选择任何页面",
+    "can_add_later": "稍后也可以添加",
+    "next": "下一步",
+    "select_from_page_tree": "从页面树选择",
     "edit_page_description": "编辑助手可以参考的页面。<br> 助手可以参考最多 {{limitLearnablePageCountPerAssistant}} 个页面,包括子页面。",
     "default_instruction": "您是这个Wiki的知识助手。\n\n## 多语言支持:\n请使用用户输入中使用的相同语言进行回复。\n",
     "add_page_button": "添加页面",

+ 1 - 1
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx

@@ -18,7 +18,7 @@ export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element
 
   return (
     <>
-      <AiAssistantManagementHeader />
+      <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.instruction" />
 
       <ModalBody className="px-4">
         <p className="text-secondary py-1">

+ 31 - 30
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx

@@ -2,20 +2,18 @@ import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { ModalBody } from 'reactstrap';
+import SimpleBar from 'simplebar-react';
 
-import type { IPageForItem } from '~/interfaces/page';
 import { useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
-import { usePageSelectModal } from '~/stores/modal';
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
-import { SelectedPageList } from './SelectedPageList';
-
+import { PageSelectionMethodButtons } from './PageSelectionMethodButtons';
+import { SelectablePageList } from './SelectablePageList';
 
 type Props = {
-  selectedPages: SelectedPage[];
-  onSelect: (page: IPageForItem, isIncludeSubPage: boolean) => void;
+  selectedPages: SelectablePage[];
   onRemove: (pageId: string) => void;
 }
 
@@ -23,35 +21,38 @@ export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { data: limitLearnablePageCountPerAssistant } = useLimitLearnablePageCountPerAssistant();
 
-  const { selectedPages, onSelect, onRemove } = props;
-
-  const { open: openPageSelectModal } = usePageSelectModal();
+  const { selectedPages, onRemove } = props;
 
-  const clickOpenPageSelectModalHandler = useCallback(() => {
-    openPageSelectModal({ onSelected: onSelect, isHierarchicalSelectionMode: true });
-  }, [onSelect, openPageSelectModal]);
+  const removePageHandler = useCallback((page: SelectablePage) => {
+    onRemove(page.path);
+  }, [onRemove]);
 
   return (
     <>
-      <AiAssistantManagementHeader />
+      <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.pages" />
 
       <ModalBody className="px-4">
-        <p
-          className="text-secondary py-1"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('modal_ai_assistant.edit_page_description', { limitLearnablePageCountPerAssistant }) }}
-        />
-
-        <button
-          type="button"
-          onClick={clickOpenPageSelectModalHandler}
-          className="btn btn-outline-primary w-100 mb-3 d-flex align-items-center justify-content-center"
-        >
-          <span className="material-symbols-outlined me-2">add</span>
-          {t('modal_ai_assistant.add_page_button')}
-        </button>
-
-        <SelectedPageList selectedPages={selectedPages} onRemove={onRemove} />
+        <div className="px-4">
+          <p
+            className="text-secondary"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('modal_ai_assistant.edit_page_description', { limitLearnablePageCountPerAssistant }) }}
+          />
+
+          <div className="mb-3">
+            <PageSelectionMethodButtons />
+          </div>
+
+          <SimpleBar style={{ maxHeight: '300px' }}>
+            <SelectablePageList
+              isEditable
+              method="delete"
+              methodButtonPosition="right"
+              pages={selectedPages}
+              onClickMethodButton={removePageHandler}
+            />
+          </SimpleBar>
+        </div>
       </ModalBody>
     </>
   );

+ 1 - 1
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx

@@ -98,7 +98,7 @@ export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
 
   return (
     <>
-      <AiAssistantManagementHeader />
+      <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.share" />
 
       <ModalBody className="px-4">
         <div className="form-check form-switch mb-4">

+ 28 - 7
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHeader.tsx

@@ -1,17 +1,31 @@
-import type { JSX } from 'react';
+import { type JSX } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { ModalHeader } from 'reactstrap';
 
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 
+type Props = {
+  labelTranslationKey: string;
+  hideBackButton?: boolean;
+  backButtonColor?: 'primary' | 'secondary';
+  backToPageMode?: AiAssistantManagementModalPageMode;
+}
+
+export const AiAssistantManagementHeader = (props: Props): JSX.Element => {
+  const {
+    labelTranslationKey,
+    hideBackButton,
+    backButtonColor = 'primary',
+    backToPageMode = AiAssistantManagementModalPageMode.HOME,
+  } = props;
 
-export const AiAssistantManagementHeader = (): JSX.Element => {
   const { t } = useTranslation();
-  const { data, close, changePageMode } = useAiAssistantManagementModal();
+  const { close, changePageMode } = useAiAssistantManagementModal();
 
   return (
     <ModalHeader
+      tag="h4"
       close={(
         <button type="button" className="btn p-0" onClick={close}>
           <span className="material-symbols-outlined">close</span>
@@ -19,10 +33,17 @@ export const AiAssistantManagementHeader = (): JSX.Element => {
       )}
     >
       <div className="d-flex align-items-center">
-        <button type="button" className="btn p-0 me-3" onClick={() => changePageMode(AiAssistantManagementModalPageMode.HOME)}>
-          <span className="material-symbols-outlined text-primary">chevron_left</span>
-        </button>
-        <span>{t(`modal_ai_assistant.page_mode_title.${data?.pageMode}`)}</span>
+        { hideBackButton
+          ? (
+            <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">growi_ai</span>
+          )
+          : (
+            <button type="button" className="btn p-0 me-3" onClick={() => changePageMode(backToPageMode)}>
+              <span className={`material-symbols-outlined text-${backButtonColor}`}>chevron_left</span>
+            </button>
+          )
+        }
+        <span className="fw-bold">{t(labelTranslationKey)}</span>
       </div>
     </ModalHeader>
   );

+ 23 - 11
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx

@@ -1,30 +1,32 @@
 import React, {
-  useCallback, useState, useMemo, type JSX,
+  useCallback, useState, useMemo, useRef, useEffect, type JSX,
 } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import {
-  ModalHeader, ModalBody, ModalFooter, Input,
+  ModalBody, ModalFooter, Input,
 } from 'reactstrap';
 
 import { AiAssistantShareScope, AiAssistantAccessScope } from '~/features/openai/interfaces/ai-assistant';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { useCurrentUser, useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import { determineShareScope } from '../../../../utils/determine-share-scope';
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 import { ShareScopeWarningModal } from './ShareScopeWarningModal';
 
 type Props = {
+  isActivePane: boolean;
   shouldEdit: boolean;
   name: string;
   description: string;
   instruction: string;
   shareScope: AiAssistantShareScope,
   accessScope: AiAssistantAccessScope,
-  selectedPages: SelectedPage[];
+  selectedPages: SelectablePage[];
   selectedUserGroupsForAccessScope: PopulatedGrantedGroup[],
   selectedUserGroupsForShareScope: PopulatedGrantedGroup[],
   onNameChange: (value: string) => void;
@@ -34,6 +36,7 @@ type Props = {
 
 export const AiAssistantManagementHome = (props: Props): JSX.Element => {
   const {
+    isActivePane,
     shouldEdit,
     name,
     description,
@@ -55,11 +58,11 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
   const [isShareScopeWarningModalOpen, setIsShareScopeWarningModalOpen] = useState(false);
 
+  const inputRef = useRef<HTMLInputElement>(null);
+
   const totalSelectedPageCount = useMemo(() => {
     return selectedPages.reduce((total, selectedPage) => {
-      const descendantCount = selectedPage.isIncludeSubPage
-        ? selectedPage.page.descendantCount ?? 0
-        : 0;
+      const descendantCount = selectedPage.descendantCount ?? 0;
       const pageCountWithDescendants = descendantCount + 1;
       return total + pageCountWithDescendants;
     }, 0);
@@ -114,12 +117,20 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
     await onUpsertAiAssistant();
   }, [accessScope, onUpsertAiAssistant, selectedUserGroupsForAccessScope, selectedUserGroupsForShareScope, shareScope]);
 
+  // Autofocus
+  useEffect(() => {
+    // Only when creating a new assistant
+    if (isActivePane && !shouldEdit) {
+      inputRef.current?.focus();
+    }
+  }, [isActivePane, shouldEdit]);
+
   return (
     <>
-      <ModalHeader tag="h4" toggle={closeAiAssistantManagementModal} className="pe-4">
-        <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">growi_ai</span>
-        <span className="fw-bold">{t(shouldEdit ? 'modal_ai_assistant.header.update_assistant' : 'modal_ai_assistant.header.add_new_assistant')}</span>
-      </ModalHeader>
+      <AiAssistantManagementHeader
+        hideBackButton
+        labelTranslationKey={shouldEdit ? 'modal_ai_assistant.header.update_assistant' : 'modal_ai_assistant.header.add_new_assistant'}
+      />
 
       <div className="px-4">
         <ModalBody>
@@ -131,6 +142,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
               className="border-0 border-bottom border-2 px-0 rounded-0"
               value={name}
               onChange={e => onNameChange(e.target.value)}
+              innerRef={inputRef}
             />
           </div>
 

+ 100 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.module.scss

@@ -0,0 +1,100 @@
+@use '@growi/core-styles/scss/variables/growi-official-colors';
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+
+.grw-ai-assistant-keyword-search :global {
+  .rbt {
+    .rbt-input-multi {
+      font-size: 1.2rem;
+      border: none;
+      border-bottom: 3px solid bs.$gray-200;
+      border-radius: 0;
+
+      &.focus {
+        border-color: var(--grw-primary-500);
+        box-shadow: none;
+      }
+    }
+
+    .rbt-menu {
+       display: none !important;
+    }
+
+    .rbt-token {
+      align-items: center;
+      justify-content: center;
+      border-radius: bs.$border-radius-xxl;
+
+      .rbt-token-label {
+        display: flex;
+        align-items: center;
+        font-weight: lighter;
+        text-align: center;
+      }
+
+      .rbt-token-remove-button {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+
+  .next-button {
+    width: 30%;
+  }
+}
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .grw-ai-assistant-keyword-search :global {
+     .rbt {
+        .rbt-token {
+          background-color: var(--grw-primary-100);
+          .rbt-token-label {
+            color: var(--grw-primary-400);
+          }
+          .rbt-token-remove-button {
+            color: var(--grw-primary-400);
+          }
+        }
+
+        .rbt-token-active {
+          background-color: var(--grw-primary-200);
+          .rbt-token-label {
+            color: var(--grw-primary-500);
+          }
+          .rbt-token-remove-button {
+            color: var(--grw-primary-500);
+          }
+        }
+     }
+  }
+}
+
+@include bs.color-mode(dark) {
+  .grw-ai-assistant-keyword-search :global {
+     .rbt {
+        .rbt-token {
+          background-color: var(--grw-primary-800);
+          .rbt-token-label {
+            color: var(--grw-primary-200);
+          }
+          .rbt-token-remove-button {
+            color: var(--grw-primary-200);
+          }
+        }
+
+        .rbt-token-active {
+          background-color: var(--grw-primary-700);
+          .rbt-token-label {
+            color: var(--grw-primary-100);
+          }
+          .rbt-token-remove-button {
+            color: var(--grw-primary-100);
+          }
+        }
+     }
+  }
+}

+ 228 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.tsx

@@ -0,0 +1,228 @@
+import React, {
+  useRef, useMemo, useCallback, useState, useEffect, type KeyboardEvent,
+} from 'react';
+
+import type { IPageHasId } from '@growi/core';
+import { isGlobPatternPath } from '@growi/core/dist/utils/page-path-utils';
+import { type TypeaheadRef, Typeahead } from 'react-bootstrap-typeahead';
+import { useTranslation } from 'react-i18next';
+import {
+  ModalBody,
+} from 'reactstrap';
+import SimpleBar from 'simplebar-react';
+
+import { useSWRxSearch } from '~/stores/search';
+
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
+import { useSelectedPages } from '../../../services/use-selected-pages';
+import {
+  useAiAssistantManagementModal, AiAssistantManagementModalPageMode,
+} from '../../../stores/ai-assistant';
+
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+import { SelectablePageList } from './SelectablePageList';
+
+import styles from './AiAssistantManagementKeywordSearch.module.scss';
+
+const moduleClass = styles['grw-ai-assistant-keyword-search'] ?? '';
+
+type SelectedSearchKeyword = {
+  id: string
+  label: string
+}
+
+const isSelectedSearchKeyword = (value: unknown): value is SelectedSearchKeyword => {
+  return (value as SelectedSearchKeyword).label != null;
+};
+
+
+type Props = {
+  isActivePane: boolean
+  baseSelectedPages: SelectablePage[],
+  updateBaseSelectedPages: (pages: SelectablePage[]) => void;
+}
+
+export const AiAssistantKeywordSearch = (props: Props): JSX.Element => {
+  const { isActivePane, baseSelectedPages, updateBaseSelectedPages } = props;
+
+  const [selectedSearchKeywords, setSelectedSearchKeywords] = useState<Array<SelectedSearchKeyword>>([]);
+  const {
+    selectedPages, selectedPagesArray, addPage, removePage,
+  } = useSelectedPages(baseSelectedPages);
+
+  const joinedSelectedSearchKeywords = useMemo(() => {
+    return selectedSearchKeywords.map(item => item.label).join(' ');
+  }, [selectedSearchKeywords]);
+
+  const { t } = useTranslation();
+  const { data: searchResult } = useSWRxSearch(joinedSelectedSearchKeywords, null, {
+    limit: 10,
+    offset: 0,
+    includeUserPages: true,
+    includeTrashPages: false,
+  });
+
+  // Search results will include subordinate pages by default
+  const pagesWithGlobPath = useMemo((): IPageHasId[] | undefined => {
+    if (searchResult == null) {
+      return;
+    }
+
+    const pages = searchResult.data.map(item => item.data);
+    return pages.map((page) => {
+      const newPage = { ...page };
+      if (newPage.path === '/') {
+        newPage.path = '/*';
+        return newPage;
+      }
+      if (!isGlobPatternPath(newPage.path)) {
+        newPage.path = `${newPage.path}/*`;
+      }
+      return newPage;
+    });
+  }, [searchResult]);
+
+  const shownSearchResult = useMemo(() => {
+    return selectedSearchKeywords.length > 0 && searchResult != null && searchResult.data.length > 0;
+  }, [searchResult, selectedSearchKeywords.length]);
+
+
+  const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
+  const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
+
+  const typeaheadRef = useRef<TypeaheadRef>(null);
+
+  const changeHandler = useCallback((selected: Array<SelectedSearchKeyword>) => {
+    setSelectedSearchKeywords(selected);
+  }, []);
+
+  const keyDownHandler = useCallback((event: KeyboardEvent<HTMLElement>) => {
+    if (event.code !== 'Space') {
+      return;
+    }
+
+    if (selectedSearchKeywords.length >= 5) {
+      return;
+    }
+
+    event.preventDefault();
+
+    // fix: https://redmine.weseek.co.jp/issues/140689
+    // "event.isComposing" is not supported
+    const isComposing = event.nativeEvent.isComposing;
+    if (isComposing) {
+      return;
+    }
+
+    const initialItem = typeaheadRef?.current?.state?.initialItem;
+    const handleMenuItemSelect = typeaheadRef?.current?._handleMenuItemSelect;
+    if (initialItem == null || handleMenuItemSelect == null) {
+      return;
+    }
+
+    if (!isSelectedSearchKeyword(initialItem)) {
+      return;
+    }
+
+    const allLabels = selectedSearchKeywords.map(item => item.label);
+    if (allLabels.includes(initialItem.label)) {
+      return;
+    }
+
+    handleMenuItemSelect(initialItem, event);
+  }, [selectedSearchKeywords]);
+
+  const nextButtonClickHandler = useCallback(() => {
+    updateBaseSelectedPages(Array.from(selectedPages.values()));
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+  }, [changePageMode, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
+
+  // Autofocus
+  useEffect(() => {
+    if (isActivePane) {
+      typeaheadRef.current?.focus();
+    }
+  }, [isActivePane]);
+
+  return (
+    <div className={moduleClass}>
+      <AiAssistantManagementHeader
+        backButtonColor="secondary"
+        backToPageMode={baseSelectedPages.length === 0 ? AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD : AiAssistantManagementModalPageMode.PAGES}
+        labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
+      />
+
+      <ModalBody className="px-4">
+        <h4 className="text-center fw-bold mb-3 mt-2">
+          {t('modal_ai_assistant.search_reference_pages_by_keyword')}
+        </h4>
+
+        <div className="px-4">
+          <Typeahead
+            allowNew
+            multiple
+            options={[]}
+            selected={selectedSearchKeywords}
+            placeholder={t('modal_ai_assistant.enter_keywords')}
+            id="ai-assistant-keyword-search"
+            ref={typeaheadRef}
+            onChange={changeHandler}
+            onKeyDown={keyDownHandler}
+          />
+
+          <label htmlFor="ai-assistant-keyword-search" className="form-text text-muted mt-2">
+            {t('modal_ai_assistant.max_items_space_separated_hint')}
+          </label>
+        </div>
+
+        { shownSearchResult && (
+          <>
+            <h4 className="text-center fw-bold mb-3 mt-4">
+              {t('modal_ai_assistant.select_assistant_reference_pages')}
+            </h4>
+            <div className="px-4">
+              <SimpleBar className="page-list-container" style={{ maxHeight: '300px' }}>
+                <SelectablePageList
+                  isEditable
+                  pages={pagesWithGlobPath ?? []}
+                  method="add"
+                  onClickMethodButton={addPage}
+                  disablePagePaths={selectedPagesArray.map(page => page.path)}
+                />
+              </SimpleBar>
+            </div>
+          </>
+        )}
+
+        <h4 className="text-center fw-bold mb-3 mt-4">
+          {t('modal_ai_assistant.reference_pages')}
+        </h4>
+
+        <div className="px-4">
+          <SimpleBar className="page-list-container" style={{ maxHeight: '300px' }}>
+            <SelectablePageList
+              pages={selectedPagesArray}
+              method="remove"
+              onClickMethodButton={removePage}
+            />
+          </SimpleBar>
+          <label className="form-text text-muted mt-2">
+            {t('modal_ai_assistant.can_add_later')}
+          </label>
+
+        </div>
+
+        <div className="d-flex justify-content-center mt-4">
+          <button
+            disabled={selectedPages.size === 0}
+            type="button"
+            className="btn btn-primary rounded next-button"
+            onClick={nextButtonClickHandler}
+          >
+            {t('modal_ai_assistant.next')}
+          </button>
+        </div>
+      </ModalBody>
+    </div>
+  );
+};

+ 49 - 23
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx

@@ -2,21 +2,23 @@ import React, {
   useCallback, useState, useEffect, type JSX,
 } from 'react';
 
+import type { IPageHasId } from '@growi/core';
 import {
   type IGrantedGroup, isPopulated,
 } from '@growi/core';
+import { isGlobPatternPath } from '@growi/core/dist/utils/page-path-utils';
 import { useTranslation } from 'react-i18next';
 import { Modal, TabContent, TabPane } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { UpsertAiAssistantData } from '~/features/openai/interfaces/ai-assistant';
 import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
-import type { IPagePathWithDescendantCount, IPageForItem } from '~/interfaces/page';
+import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { useSWRxPagePathsWithDescendantCount } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import { removeGlobPath } from '../../../../utils/remove-glob-path';
 import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
 import {
@@ -30,6 +32,9 @@ import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEdi
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
 import { AiAssistantManagementEditShare } from './AiAssistantManagementEditShare';
 import { AiAssistantManagementHome } from './AiAssistantManagementHome';
+import { AiAssistantKeywordSearch } from './AiAssistantManagementKeywordSearch';
+import { AiAssistantManagementPageSelectionMethod } from './AiAssistantManagementPageSelectionMethod';
+import { AiAssistantManagementPageTreeSelection } from './AiAssistantManagementPageTreeSelection';
 
 import styles from './AiAssistantManagementModal.module.scss';
 
@@ -51,14 +56,13 @@ const convertToPopulatedGrantedGroups = (selectedGroups: IGrantedGroup[]): Popul
   return populatedGrantedGroups;
 };
 
-const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectedPage[] => {
+const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectablePage[] => {
   return pagePathPatterns.map((pagePathPattern) => {
-    const isIncludeSubPage = pagePathPattern.endsWith('/*');
-    const path = isIncludeSubPage ? pagePathPattern.slice(0, -2) : pagePathPattern;
-    const page = pagePathsWithDescendantCount.find(page => page.path === path);
+    const pathWithoutGlob = isGlobPatternPath(pagePathPattern) ? pagePathPattern.slice(0, -2) : pagePathPattern;
+    const page = pagePathsWithDescendantCount.find(p => p.path === pathWithoutGlob);
     return {
-      page: page ?? { path },
-      isIncludeSubPage,
+      ...page,
+      path: pagePathPattern,
     };
   });
 };
@@ -88,7 +92,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const [selectedAccessScope, setSelectedAccessScope] = useState<AiAssistantAccessScope>(AiAssistantAccessScope.OWNER);
   const [selectedUserGroupsForAccessScope, setSelectedUserGroupsForAccessScope] = useState<PopulatedGrantedGroup[]>([]);
   const [selectedUserGroupsForShareScope, setSelectedUserGroupsForShareScope] = useState<PopulatedGrantedGroup[]>([]);
-  const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
+  const [selectedPages, setSelectedPages] = useState<SelectablePage[]>([]);
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
 
 
@@ -113,6 +117,14 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   }, [aiAssistant?.pagePathPatterns, pagePathsWithDescendantCount, shouldEdit]);
 
 
+  /*
+  *  For AiAssistantManagementKeywordSearch & AiAssistantManagementPageTreeSelection methods
+  */
+  const selectPageHandler = useCallback((pages: IPageHasId[]) => {
+    setSelectedPages(pages);
+  }, []);
+
+
   /*
   *  For AiAssistantManagementHome methods
   */
@@ -127,8 +139,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const upsertAiAssistantHandler = useCallback(async() => {
     try {
       const pagePathPatterns = selectedPages
-        .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
-        .filter((path): path is string => path !== undefined && path !== null);
+        .map(selectedPage => selectedPage.path);
 
       const grantedGroupsForShareScope = selectedShareScope === AiAssistantShareScope.GROUPS
         ? convertToGrantedGroups(selectedUserGroupsForShareScope)
@@ -167,8 +178,11 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
       toastError(shouldEdit ? t('modal_ai_assistant.toaster.update_failed') : t('modal_ai_assistant.toaster.create_failed'));
       logger.error(err);
     }
-  // eslint-disable-next-line max-len
-  }, [t, selectedPages, selectedShareScope, selectedUserGroupsForShareScope, selectedAccessScope, selectedUserGroupsForAccessScope, name, description, instruction, shouldEdit, aiAssistant?._id, mutateAiAssistants, closeAiAssistantManagementModal]);
+  }, [
+    selectedPages, selectedShareScope, selectedUserGroupsForShareScope, selectedAccessScope,
+    selectedUserGroupsForAccessScope, name, description, instruction, shouldEdit, t, mutateAiAssistants,
+    closeAiAssistantManagementModal, aiAssistant?._id, aiAssistantSidebarData?.aiAssistantData?._id, refreshAiAssistantData,
+  ]);
 
 
   /*
@@ -210,15 +224,8 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   /*
   *  For AiAssistantManagementEditPages methods
   */
-  const selectPageHandler = useCallback((page: IPageForItem, isIncludeSubPage: boolean) => {
-    const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page.path);
-    if (page.path != null && !selectedPageIds.includes(page.path)) {
-      setSelectedPages([...selectedPages, { page, isIncludeSubPage }]);
-    }
-  }, [selectedPages]);
-
   const removePageHandler = useCallback((pagePath: string) => {
-    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page.path !== pagePath));
+    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.path !== pagePath));
   }, [selectedPages]);
 
 
@@ -236,8 +243,28 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   return (
     <>
       <TabContent activeTab={pageMode}>
+        <TabPane tabId={AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD}>
+          <AiAssistantManagementPageSelectionMethod />
+        </TabPane>
+
+        <TabPane tabId={AiAssistantManagementModalPageMode.KEYWORD_SEARCH}>
+          <AiAssistantKeywordSearch
+            isActivePane={pageMode === AiAssistantManagementModalPageMode.KEYWORD_SEARCH}
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
+          />
+        </TabPane>
+
+        <TabPane tabId={AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION}>
+          <AiAssistantManagementPageTreeSelection
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
+          />
+        </TabPane>
+
         <TabPane tabId={AiAssistantManagementModalPageMode.HOME}>
           <AiAssistantManagementHome
+            isActivePane={pageMode === AiAssistantManagementModalPageMode.HOME}
             shouldEdit={shouldEdit}
             name={name}
             description={description}
@@ -269,7 +296,6 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGES}>
           <AiAssistantManagementEditPages
             selectedPages={selectedPages}
-            onSelect={selectPageHandler}
             onRemove={removePageHandler}
           />
         </TabPane>
@@ -293,7 +319,7 @@ export const AiAssistantManagementModal = (): JSX.Element => {
   const isOpened = aiAssistantManagementModalData?.isOpened ?? false;
 
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeAiAssistantManagementModal} className={moduleClass} scrollable>
+    <Modal size="lg" isOpen={isOpened} toggle={closeAiAssistantManagementModal} className={moduleClass}>
       { isOpened && (
         <AiAssistantManagementModalSubstance />
       ) }

+ 35 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageSelectionMethod.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  ModalBody,
+} from 'reactstrap';
+
+import { useAiAssistantManagementModal } from '../../../stores/ai-assistant';
+
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+import { PageSelectionMethodButtons } from './PageSelectionMethodButtons';
+
+export const AiAssistantManagementPageSelectionMethod = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
+  const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
+
+  return (
+    <>
+      <AiAssistantManagementHeader
+        hideBackButton={isNewAiAssistant}
+        labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
+      />
+
+      <ModalBody className="px-4">
+        <h4 className="text-center fw-bold mb-4 mt-2">
+          {t('modal_ai_assistant.select_source_pages')}
+        </h4>
+
+        <PageSelectionMethodButtons />
+
+      </ModalBody>
+    </>
+  );
+};

+ 11 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.module.scss

@@ -0,0 +1,11 @@
+.grw-ai-assistant-management-page-tree-selection :global {
+  .next-button {
+    width: 30%;
+  }
+
+  .page-tree-item {
+    .list-group-item {
+      padding: 0.4rem 1rem !important;
+    }
+  }
+}

+ 171 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx

@@ -0,0 +1,171 @@
+import React, {
+  Suspense, useCallback, memo,
+} from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  ModalBody,
+} from 'reactstrap';
+import SimpleBar from 'simplebar-react';
+
+import { ItemsTree } from '~/client/components/ItemsTree';
+import ItemsTreeContentSkeleton from '~/client/components/ItemsTree/ItemsTreeContentSkeleton';
+import type { TreeItemProps } from '~/client/components/TreeItem';
+import { TreeItemLayout } from '~/client/components/TreeItem';
+import type { IPageForItem } from '~/interfaces/page';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
+
+import { type SelectablePage, isSelectablePage } from '../../../../interfaces/selectable-page';
+import { useSelectedPages } from '../../../services/use-selected-pages';
+import { AiAssistantManagementModalPageMode, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
+
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+import { SelectablePageList } from './SelectablePageList';
+
+import styles from './AiAssistantManagementPageTreeSelection.module.scss';
+
+const moduleClass = styles['grw-ai-assistant-management-page-tree-selection'] ?? '';
+
+const SelectablePageTree = memo((props: { onClickAddPageButton: (page: SelectablePage) => void }) => {
+  const { onClickAddPageButton } = props;
+
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const pageTreeItemClickHandler = useCallback((page: IPageForItem) => {
+    if (!isSelectablePage(page)) {
+      return;
+    }
+
+    onClickAddPageButton(page);
+  }, [onClickAddPageButton]);
+
+  const PageTreeItem = (props: TreeItemProps) => {
+    const { itemNode } = props;
+    const { page } = itemNode;
+
+    const SelectPageButton = () => {
+      return (
+        <button
+          type="button"
+          className="border-0 rounded btn p-0"
+          onClick={(e) => {
+            e.stopPropagation();
+            pageTreeItemClickHandler(page);
+          }}
+        >
+          <span className="material-symbols-outlined p-0 me-2 text-primary">add_circle</span>
+        </button>
+      );
+    };
+
+    return (
+      <TreeItemLayout
+        {...props}
+        itemClass={PageTreeItem}
+        className="text-muted"
+        customHoveredEndComponents={[SelectPageButton]}
+      />
+    );
+  };
+
+  return (
+    <div className="page-tree-item">
+      <ItemsTree
+        targetPath="/"
+        isEnableActions={!isGuestUser}
+        isReadOnlyUser={!!isReadOnlyUser}
+        CustomTreeItem={PageTreeItem}
+      />
+    </div>
+  );
+});
+
+type Props = {
+  baseSelectedPages: SelectablePage[],
+  updateBaseSelectedPages: (pages: SelectablePage[]) => void;
+}
+
+export const AiAssistantManagementPageTreeSelection = (props: Props): JSX.Element => {
+  const { baseSelectedPages, updateBaseSelectedPages } = props;
+
+  const { t } = useTranslation();
+  const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
+  const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
+
+  const {
+    selectedPages, selectedPagesRef, selectedPagesArray, addPage, removePage,
+  } = useSelectedPages(baseSelectedPages);
+
+
+  const addPageButtonClickHandler = useCallback((page: SelectablePage) => {
+    const pagePathWithGlob = `${page.path}/*`;
+    if (selectedPagesRef.current == null || selectedPagesRef.current.has(pagePathWithGlob)) {
+      return;
+    }
+
+    const clonedPage = { ...page };
+    clonedPage.path = pagePathWithGlob;
+
+    addPage(clonedPage);
+  }, [
+    addPage,
+    selectedPagesRef, // Prevent flickering (use ref to avoid method recreation)
+  ]);
+
+  const nextButtonClickHandler = useCallback(() => {
+    updateBaseSelectedPages(Array.from(selectedPages.values()));
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+  }, [changePageMode, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
+
+  return (
+    <div className={moduleClass}>
+      <AiAssistantManagementHeader
+        backButtonColor="secondary"
+        backToPageMode={baseSelectedPages.length === 0 ? AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD : AiAssistantManagementModalPageMode.PAGES}
+        labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
+      />
+
+      <ModalBody className="px-4">
+        <h4 className="text-center fw-bold mb-3 mt-2">
+          {t('modal_ai_assistant.search_reference_pages_by_keyword')}
+        </h4>
+
+        <Suspense fallback={<ItemsTreeContentSkeleton />}>
+          <div className="px-4">
+            <SelectablePageTree onClickAddPageButton={addPageButtonClickHandler} />
+          </div>
+        </Suspense>
+
+        <h4 className="text-center fw-bold mb-3 mt-4">
+          {t('modal_ai_assistant.reference_pages')}
+        </h4>
+
+        <div className="px-4">
+          <SimpleBar className="page-list-container" style={{ maxHeight: '300px' }}>
+            <SelectablePageList
+              method="remove"
+              methodButtonPosition="right"
+              pages={selectedPagesArray}
+              onClickMethodButton={removePage}
+            />
+          </SimpleBar>
+          <label className="form-text text-muted mt-2">
+            {t('modal_ai_assistant.can_add_later')}
+          </label>
+        </div>
+
+        <div className="d-flex justify-content-center mt-4">
+          <button
+            type="button"
+            className="btn btn-primary rounded next-button"
+            disabled={selectedPages.size === 0}
+            onClick={nextButtonClickHandler}
+          >
+            {t('modal_ai_assistant.next')}
+          </button>
+        </div>
+      </ModalBody>
+    </div>
+  );
+};

+ 28 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.module.scss

@@ -0,0 +1,28 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// == Colors
+.page-selection-method-buttons :global {
+  .page-selection-method-btn {
+      &:hover {
+        color: var(--bs-primary);
+        background-color: rgba(var(--bs-primary-rgb), 0.1);
+        border-color: var(--bs-primary) !important;
+      }
+    }
+}
+
+@include bs.color-mode(light) {
+  .page-selection-method-buttons :global {
+    .page-selection-method-btn {
+      border: 2px solid bs.$gray-300;
+    }
+  }
+}
+
+@include bs.color-mode(dark) {
+  .page-selection-method-buttons :global {
+    .page-selection-method-btn {
+      border: 2px solid bs.$gray-700;
+    }
+  }
+}

+ 55 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+
+import styles from './PageSelectionMethodButtons.module.scss';
+
+const moduleClass = styles['page-selection-method-buttons'] ?? '';
+
+const SelectionButton = (props: { icon: string, label: string, onClick: () => void }): JSX.Element => {
+  const { icon, label, onClick } = props;
+
+  return (
+    <button
+      type="button"
+      className="btn text-center py-4 w-100 page-selection-method-btn"
+      onClick={onClick}
+    >
+      <span
+        className="material-symbols-outlined d-block mb-3 fs-1"
+      >
+        {icon}
+      </span>
+      <div>{label}</div>
+    </button>
+  );
+};
+
+
+export const PageSelectionMethodButtons = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { changePageMode } = useAiAssistantManagementModal();
+
+  return (
+    <div className={moduleClass}>
+      <div className="row g-3">
+        <div className="col">
+          <SelectionButton
+            icon="manage_search"
+            label={t('modal_ai_assistant.search_by_keyword')}
+            onClick={() => changePageMode(AiAssistantManagementModalPageMode.KEYWORD_SEARCH)}
+          />
+        </div>
+        <div className="col">
+          <SelectionButton
+            icon="account_tree"
+            label={t('modal_ai_assistant.select_from_page_tree')}
+            onClick={() => changePageMode(AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION)}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};

+ 43 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePageList.module.scss

@@ -0,0 +1,43 @@
+@use '~/styles/variables' as var;
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+ .selectable-page-list :global {
+    .page-path {
+      display: inline-block;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      vertical-align: middle;
+      border: 2px solid transparent;
+    }
+
+    .page-path-editable {
+      cursor: pointer;
+      &:hover {
+        border-color: var(--bs-primary-border-subtle);
+      }
+    }
+
+    .page-path-input {
+      border: 2px solid var(--bs-border-color);
+    }
+}
+
+
+// == Colors
+@include bs.color-mode(light) {
+  .selectable-page-list :global {
+    .page-list-item {
+      background-color: #{bs.$gray-100};
+    }
+  }
+}
+
+@include bs.color-mode(dark) {
+  .selectable-page-page-list :global {
+    .page-list-item {
+      background-color: #{bs.$gray-900};
+    }
+  }
+}

+ 249 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePageList.tsx

@@ -0,0 +1,249 @@
+import React, {
+  useMemo, memo, useState, useCallback, useRef, useEffect,
+} from 'react';
+
+import { pathUtils } from '@growi/core/dist/utils';
+import { useRect } from '@growi/ui/dist/utils';
+import { useTranslation } from 'react-i18next';
+import AutosizeInput from 'react-input-autosize';
+
+import { type SelectablePage } from '../../../../interfaces/selectable-page';
+import { isCreatablePagePathPattern } from '../../../../utils/is-creatable-page-path-pattern';
+
+import styles from './SelectablePageList.module.scss';
+
+const moduleClass = styles['selectable-page-list'] ?? '';
+
+type MethodButtonProps = {
+  page: SelectablePage;
+  disablePagePaths: string[];
+  method: 'add' | 'remove' | 'delete'
+  onClickMethodButton: (page: SelectablePage) => void;
+}
+
+const MethodButton = memo((props: MethodButtonProps) => {
+  const {
+    page,
+    disablePagePaths,
+    method,
+    onClickMethodButton,
+  } = props;
+
+  const iconName = useMemo(() => {
+    switch (method) {
+      case 'add':
+        return 'add_circle';
+      case 'remove':
+        return 'do_not_disturb_on';
+      case 'delete':
+        return 'delete';
+      default:
+        return '';
+    }
+  }, [method]);
+
+  const color = useMemo(() => {
+    switch (method) {
+      case 'add':
+        return 'text-primary';
+      case 'remove':
+        return 'text-secondary';
+      case 'delete':
+        return 'text-secondary';
+      default:
+        return '';
+    }
+  }, [method]);
+
+  return (
+    <button
+      type="button"
+      className={`btn border-0 ${color}`}
+      disabled={disablePagePaths.includes(page.path)}
+      onClick={(e) => {
+        e.stopPropagation();
+        onClickMethodButton(page);
+      }}
+    >
+      <span className="material-symbols-outlined">
+        {iconName}
+      </span>
+    </button>
+  );
+});
+
+
+type EditablePagePathProps = {
+  isEditable?: boolean;
+  page: SelectablePage;
+  disablePagePaths: string[];
+  methodButtonPosition?: 'left' | 'right';
+}
+
+const EditablePagePath = memo((props: EditablePagePathProps): JSX.Element => {
+  const {
+    page,
+    isEditable,
+    disablePagePaths = [],
+    methodButtonPosition = 'left',
+  } = props;
+
+  const [editingPagePath, setEditingPagePath] = useState<string | null>(null);
+  const [inputValue, setInputValue] = useState('');
+
+  const inputRef = useRef<HTMLInputElement & AutosizeInput | null>(null);
+  const editingContainerRef = useRef<HTMLDivElement>(null);
+  const [editingContainerRect] = useRect(editingContainerRef);
+
+  const isEditing = isEditable && editingPagePath === page.path;
+
+  const handlePagePathClick = useCallback((page: SelectablePage) => {
+    if (!isEditable || disablePagePaths.includes(page.path)) {
+      return;
+    }
+    setEditingPagePath(page.path);
+    setInputValue(page.path);
+  }, [disablePagePaths, isEditable]);
+
+  const handleInputBlur = useCallback(() => {
+    setEditingPagePath(null);
+  }, []);
+
+  const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === 'Enter') {
+
+      // Validate page path
+      const pagePathWithSlash = pathUtils.addHeadingSlash(inputValue);
+      if (inputValue === '' || disablePagePaths.includes(pagePathWithSlash) || !isCreatablePagePathPattern(pagePathWithSlash)) {
+        handleInputBlur();
+        return;
+      }
+
+      // Update page path
+      page.path = pagePathWithSlash;
+
+      handleInputBlur();
+    }
+  }, [disablePagePaths, handleInputBlur, inputValue, page]);
+
+  // Autofocus
+  useEffect(() => {
+    if (editingPagePath != null && inputRef.current != null) {
+      inputRef.current.focus();
+    }
+  }, [editingPagePath]);
+
+  return (
+    <div
+      ref={editingContainerRef}
+      className={`flex-grow-1 ${methodButtonPosition === 'left' ? 'me-2' : 'mx-2'}`}
+      style={{ minWidth: 0 }}
+    >
+      {isEditing
+        ? (
+          <AutosizeInput
+            id="page-path-input"
+            inputClassName="page-path-input"
+            type="text"
+            ref={inputRef}
+            value={inputValue}
+            onBlur={handleInputBlur}
+            onChange={e => setInputValue(e.target.value)}
+            onKeyDown={handleInputKeyDown}
+            inputStyle={{ maxWidth: (editingContainerRect?.width ?? 0) - 10 }}
+          />
+        )
+        : (
+          <span
+            className={`page-path ${isEditable && !disablePagePaths.includes(page.path) ? 'page-path-editable' : ''}`}
+            onClick={() => handlePagePathClick(page)}
+            title={page.path}
+          >
+            {page.path}
+          </span>
+        )}
+    </div>
+  );
+});
+
+
+type SelectablePageListProps = {
+  pages: SelectablePage[],
+  method: 'add' | 'remove' | 'delete'
+  methodButtonPosition?: 'left' | 'right',
+  disablePagePaths?: string[],
+  isEditable?: boolean,
+  onClickMethodButton: (page: SelectablePage) => void,
+}
+
+export const SelectablePageList = (props: SelectablePageListProps): JSX.Element => {
+  const {
+    pages,
+    method,
+    methodButtonPosition = 'left',
+    disablePagePaths = [],
+    isEditable,
+    onClickMethodButton,
+  } = props;
+
+  const { t } = useTranslation();
+
+  if (pages.length === 0) {
+    return (
+      <div className={moduleClass}>
+        <div className="border-0 text-center page-list-item rounded py-3">
+          <p className="text-muted mb-0">{t('modal_ai_assistant.no_pages_selected')}</p>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className={`list-group ${moduleClass}`}>
+      {pages.map((page) => {
+        return (
+          <div
+            key={page.path}
+            className="list-group-item border-0 page-list-item d-flex align-items-center p-1 mb-2 rounded"
+          >
+
+            {methodButtonPosition === 'left'
+              && (
+                <MethodButton
+                  page={page}
+                  method={method}
+                  disablePagePaths={disablePagePaths}
+                  onClickMethodButton={onClickMethodButton}
+                />
+              )
+            }
+
+            <EditablePagePath
+              page={page}
+              isEditable={isEditable}
+              disablePagePaths={disablePagePaths}
+              methodButtonPosition={methodButtonPosition}
+            />
+
+            <span className={`badge bg-body-secondary rounded-pill ${methodButtonPosition === 'left' ? 'me-2' : ''}`}>
+              <span className="text-body-tertiary">
+                {page.descendantCount}
+              </span>
+            </span>
+
+            {methodButtonPosition === 'right'
+              && (
+                <MethodButton
+                  page={page}
+                  method={method}
+                  disablePagePaths={disablePagePaths}
+                  onClickMethodButton={onClickMethodButton}
+                />
+              )
+            }
+          </div>
+        );
+      })}
+    </div>
+  );
+};

+ 0 - 43
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectedPageList.tsx

@@ -1,43 +0,0 @@
-import { memo } from 'react';
-
-import type { SelectedPage } from '../../../../interfaces/selected-page';
-
-type SelectedPageListProps = {
-  selectedPages: SelectedPage[];
-  onRemove?: (pagePath?: string) => void;
-};
-
-const SelectedPageListBase: React.FC<SelectedPageListProps> = ({ selectedPages, onRemove }: SelectedPageListProps) => {
-  if (selectedPages.length === 0) {
-    return <></>;
-  }
-
-  return (
-    <div className="mb-3">
-      {selectedPages.map(({ page, isIncludeSubPage }) => (
-        <div
-          key={page.path}
-          className="mb-2 d-flex justify-content-between align-items-center bg-body-tertiary rounded py-2 px-3"
-        >
-          <div className="d-flex align-items-center overflow-hidden text-body">
-            { isIncludeSubPage
-              ? <>{`${page.path}/*`}</>
-              : <>{page.path}</>
-            }
-          </div>
-          {onRemove != null && page.path != null && (
-            <button
-              type="button"
-              className="btn p-0 ms-3 text-body-secondary"
-              onClick={() => onRemove(page.path)}
-            >
-              <span className="material-symbols-outlined fs-4">delete</span>
-            </button>
-          )}
-        </div>
-      ))}
-    </div>
-  );
-};
-
-export const SelectedPageList = memo(SelectedPageListBase);

+ 4 - 4
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx

@@ -5,11 +5,11 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 
 type Props = {
   isOpen: boolean,
-  selectedPages: SelectedPage[],
+  selectedPages: SelectablePage[],
   closeModal: () => void,
   onSubmit: () => Promise<void>,
 }
@@ -48,8 +48,8 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
         <div className="mb-4">
           <p className="mb-2 text-secondary">{t('share_scope_warning_modal.selected_pages_label')}</p>
           {selectedPages.map(selectedPage => (
-            <code key={selectedPage.page.path}>
-              {selectedPage.page.path}
+            <code key={selectedPage.path}>
+              {selectedPage.path}
             </code>
           ))}
         </div>

+ 1 - 1
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx

@@ -183,7 +183,7 @@ export const AiAssistantList: React.FC<AiAssistantListProps> = ({
         </h3>
         <span
           className="material-symbols-outlined"
-        >{`keyboard_arrow_${isCollapsed ? 'up' : 'down'}`}
+        >{`keyboard_arrow_${isCollapsed ? 'down' : 'right'}`}
         </span>
       </button>
 

+ 72 - 0
apps/app/src/features/openai/client/services/use-selected-pages.tsx

@@ -0,0 +1,72 @@
+import {
+  useState, useCallback, useEffect, useMemo, useRef,
+} from 'react';
+
+import type { SelectablePage } from '../../interfaces/selectable-page';
+import { useAiAssistantManagementModal } from '../stores/ai-assistant';
+
+
+type UseSelectedPages = {
+  selectedPages: Map<string, SelectablePage>,
+  selectedPagesRef: React.RefObject<Map<string, SelectablePage>>,
+  selectedPagesArray: SelectablePage[],
+  addPage: (page: SelectablePage) => void,
+  removePage: (page: SelectablePage) => void,
+}
+
+export const useSelectedPages = (initialPages?: SelectablePage[]): UseSelectedPages => {
+  const [selectedPages, setSelectedPages] = useState<Map<string, SelectablePage>>(new Map());
+  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
+
+  const selectedPagesRef = useRef(selectedPages);
+
+  const selectedPagesArray = useMemo(() => {
+    return Array.from(selectedPages.values());
+  }, [selectedPages]);
+
+  useEffect(() => {
+    selectedPagesRef.current = selectedPages;
+  }, [selectedPages]);
+
+  useEffect(() => {
+    // Initialize each time PageMode is changed
+    if (initialPages != null && aiAssistantManagementModalData?.pageMode != null) {
+      const initialMap = new Map<string, SelectablePage>();
+      initialPages.forEach((page) => {
+        if (page.path != null) {
+          initialMap.set(page.path, page);
+        }
+      });
+      setSelectedPages(initialMap);
+    }
+  }, [aiAssistantManagementModalData?.pageMode, initialPages]);
+
+  const addPage = useCallback((page: SelectablePage) => {
+    setSelectedPages((prev) => {
+      const newMap = new Map(prev);
+      if (page.path != null) {
+        newMap.set(page.path, page);
+      }
+      return newMap;
+    });
+  }, []);
+
+  const removePage = useCallback((page: SelectablePage) => {
+    setSelectedPages((prev) => {
+      const newMap = new Map(prev);
+      if (page.path != null) {
+        newMap.delete(page.path);
+      }
+      return newMap;
+    });
+  }, []);
+
+
+  return {
+    selectedPages,
+    selectedPagesRef,
+    selectedPagesArray,
+    addPage,
+    removePage,
+  };
+};

+ 17 - 2
apps/app/src/features/openai/client/stores/ai-assistant.tsx

@@ -9,14 +9,21 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import { type AccessibleAiAssistantsHasId, type AiAssistantHasId } from '../../interfaces/ai-assistant';
 import type { IThreadRelationHasId } from '../../interfaces/thread-relation'; // IThreadHasId を削除
 
+
+/*
+*  useAiAssistantManagementModal
+*/
 export const AiAssistantManagementModalPageMode = {
   HOME: 'home',
   SHARE: 'share',
   PAGES: 'pages',
   INSTRUCTION: 'instruction',
+  PAGE_SELECTION_METHOD: 'page-selection-method',
+  KEYWORD_SEARCH: 'keyword-search',
+  PAGE_TREE_SELECTION: 'page-tree-selection',
 } as const;
 
-type AiAssistantManagementModalPageMode = typeof AiAssistantManagementModalPageMode[keyof typeof AiAssistantManagementModalPageMode];
+export type AiAssistantManagementModalPageMode = typeof AiAssistantManagementModalPageMode[keyof typeof AiAssistantManagementModalPageMode];
 
 type AiAssistantManagementModalStatus = {
   isOpened: boolean,
@@ -38,7 +45,15 @@ export const useAiAssistantManagementModal = (
 
   return {
     ...swrResponse,
-    open: useCallback((aiAssistantData) => { swrResponse.mutate({ isOpened: true, aiAssistantData }) }, [swrResponse]),
+    open: useCallback((aiAssistantData) => {
+      swrResponse.mutate({
+        isOpened: true,
+        aiAssistantData,
+        pageMode: aiAssistantData != null
+          ? AiAssistantManagementModalPageMode.HOME
+          : AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD,
+      });
+    }, [swrResponse]),
     close: useCallback(() => swrResponse.mutate({ isOpened: false, aiAssistantData: undefined }), [swrResponse]),
     changePageMode: useCallback((pageMode: AiAssistantManagementModalPageMode) => {
       swrResponse.mutate({ isOpened: swrResponse.data?.isOpened ?? false, pageMode, aiAssistantData: swrResponse.data?.aiAssistantData });

+ 10 - 0
apps/app/src/features/openai/interfaces/selectable-page.ts

@@ -0,0 +1,10 @@
+import type { IPageHasId } from '@growi/core';
+
+import type { IPageForItem } from '~/interfaces/page';
+
+export type SelectablePage = Partial<IPageHasId> & { path: string }
+
+// type guard
+export const isSelectablePage = (page: IPageForItem): page is SelectablePage => {
+  return page.path != null;
+};

+ 0 - 6
apps/app/src/features/openai/interfaces/selected-page.ts

@@ -1,6 +0,0 @@
-import type { IPageForItem } from '~/interfaces/page';
-
-export type SelectedPage = {
-  page: IPageForItem,
-  isIncludeSubPage: boolean,
-}

+ 2 - 7
apps/app/src/features/openai/server/routes/middlewares/upsert-ai-assistant-validator.ts

@@ -1,6 +1,6 @@
 import { GroupType } from '@growi/core';
-import { isGlobPatternPath, isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
 import { type ValidationChain, body } from 'express-validator';
+import { isCreatablePagePathPattern } from '../../../utils/is-creatable-page-path-pattern';
 
 import { AiAssistantShareScope, AiAssistantAccessScope } from '../../../interfaces/ai-assistant';
 
@@ -42,12 +42,7 @@ export const upsertAiAssistantValidator: ValidationChain[] = [
     .notEmpty()
     .withMessage('pagePathPatterns must not be empty')
     .custom((value: string) => {
-      // check if the value is a glob pattern path
-      if (value.includes('*')) {
-        return isGlobPatternPath(value) && isCreatablePage(value.replaceAll('*', ''));
-      }
-
-      return isCreatablePage(value);
+      return isCreatablePagePathPattern(value);
     }),
 
   body('grantedGroupsForShareScope')

+ 13 - 0
apps/app/src/features/openai/utils/is-creatable-page-path-pattern.ts

@@ -0,0 +1,13 @@
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { removeGlobPath } from './remove-glob-path';
+
+export const isCreatablePagePathPattern = (pagePath: string): boolean => {
+  const isGlobPattern = pagePathUtils.isGlobPatternPath(pagePath);
+  if (isGlobPattern) {
+    // Remove glob pattern since glob paths are non-creatable in GROWI
+    const pathWithoutGlob = removeGlobPath([pagePath])[0];
+    return pagePathUtils.isCreatablePage(pathWithoutGlob);
+  }
+
+  return pagePathUtils.isCreatablePage(pagePath);
+};

+ 3 - 3
apps/app/src/linter-checker/test.js

@@ -2,7 +2,7 @@
  * VSCode の Eslint 設定チェック方法
  *
  * 1. .eslilntignore ファイル中の `/src/linter-checker/**` 行を消す
- * 
+ *
  * 2. VSCode で以下のエラーが表示されていることを確認
  *   - constructor で eslint(space-before-blocks)
  *   - ファイル末尾の ";" で eslint(eol-last)
@@ -15,9 +15,9 @@
  *
  */
 class EslintTest {
-  constructor(){
+  constructor() {
     this.i = 0;
   }
 }
 
-module.exports = EslintTest;
+module.exports = EslintTest;

+ 3 - 2
apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js

@@ -3,12 +3,13 @@ import mongoose from 'mongoose';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:remove-index-for-ns-from-configs');
 
 async function dropIndexIfExists(db, collectionName, indexName) {
   // check existence of the collection
-  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  const items = await db
+    .listCollections({ name: collectionName }, { nameOnly: true })
+    .toArray();
   if (items.length === 0) {
     return;
   }

+ 3 - 2
apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js

@@ -5,7 +5,6 @@ import { configManager } from '~/server/service/config-manager';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:generate-service-instance-id');
 
 module.exports = {
@@ -15,7 +14,9 @@ module.exports = {
 
     await configManager.loadConfigs();
 
-    await configManager.updateConfig('app:serviceInstanceId', uuidv4(), { skipPubsub: true });
+    await configManager.updateConfig('app:serviceInstanceId', uuidv4(), {
+      skipPubsub: true,
+    });
   },
 
   async down() {

+ 0 - 3
apps/app/src/migrations/20180926134048-make-email-unique.js

@@ -4,11 +4,9 @@ import userModelFactory from '~/server/models/user';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:make-email-unique');
 
 module.exports = {
-
   async up(db, next) {
     logger.info('Start migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
@@ -44,5 +42,4 @@ module.exports = {
     // do not rollback
     next();
   },
-
 };

+ 13 - 9
apps/app/src/migrations/20180927102719-init-serverurl.js

@@ -12,12 +12,11 @@ const logger = loggerFactory('growi:migrate:init-serverurl');
  */
 function isAllValuesSame(array) {
   return !!array.reduce((a, b) => {
-    return (a === b) ? a : NaN;
+    return a === b ? a : NaN;
   });
 }
 
 module.exports = {
-
   async up(db) {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
@@ -28,7 +27,9 @@ module.exports = {
     });
     // exit if exists
     if (siteUrlConfig != null) {
-      logger.info('\'app:siteUrl\' is already exists. This migration terminates without any changes.');
+      logger.info(
+        "'app:siteUrl' is already exists. This migration terminates without any changes.",
+      );
       return;
     }
 
@@ -48,11 +49,15 @@ module.exports = {
       logger.info(configs);
 
       // extract domain
-      const siteUrls = configs.map((config) => {
-        // see https://regex101.com/r/Q0Isjo/2
-        const match = config.value.match(/^"(https?:\/\/[^/]+).*"$/);
-        return (match != null) ? match[1] : null;
-      }).filter((value) => { return value != null });
+      const siteUrls = configs
+        .map((config) => {
+          // see https://regex101.com/r/Q0Isjo/2
+          const match = config.value.match(/^"(https?:\/\/[^/]+).*"$/);
+          return match != null ? match[1] : null;
+        })
+        .filter((value) => {
+          return value != null;
+        });
 
       // determine serverUrl if all values are same
       if (siteUrls.length > 0 && isAllValuesSame(siteUrls)) {
@@ -82,5 +87,4 @@ module.exports = {
 
     logger.info('Migration has been successfully rollbacked');
   },
-
 };

+ 12 - 7
apps/app/src/migrations/20181019114028-abolish-page-group-relation.js

@@ -5,11 +5,12 @@ import userGroupModelFactory from '~/server/models/user-group';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 
 async function isCollectionExists(db, collectionName) {
-  const collections = await db.listCollections({ name: collectionName }).toArray();
+  const collections = await db
+    .listCollections({ name: collectionName })
+    .toArray();
   return collections.length > 0;
 }
 
@@ -28,14 +29,16 @@ async function isCollectionExists(db, collectionName) {
  *   - Page model has 'grantedGroup' field newly
  */
 module.exports = {
-
   async up(db) {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
+    const isPagegrouprelationsExists = await isCollectionExists(
+      db,
+      'pagegrouprelations',
+    );
     if (!isPagegrouprelationsExists) {
-      logger.info("'pagegrouprelations' collection doesn't exist");   // eslint-disable-line
+      logger.info("'pagegrouprelations' collection doesn't exist"); // eslint-disable-line
       logger.info('Migration has successfully applied');
       return;
     }
@@ -44,7 +47,10 @@ module.exports = {
     const UserGroup = userGroupModelFactory();
 
     // retrieve all documents from 'pagegrouprelations'
-    const relations = await db.collection('pagegrouprelations').find().toArray();
+    const relations = await db
+      .collection('pagegrouprelations')
+      .find()
+      .toArray();
 
     /* eslint-disable no-await-in-loop */
     for (const relation of relations) {
@@ -115,5 +121,4 @@ module.exports = {
 
     logger.info('Migration has been successfully rollbacked');
   },
-
 };

+ 3 - 2
apps/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -16,7 +16,6 @@ const logger = loggerFactory('growi:migrate:add-config-app-installed');
  *     - value will be false if no users exist
  */
 module.exports = {
-
   async up(db) {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
@@ -29,7 +28,9 @@ module.exports = {
     });
     // exit if exists
     if (appInstalled != null) {
-      logger.info('\'app:appInstalled\' is already exists. This migration terminates without any changes.');
+      logger.info(
+        "'app:appInstalled' is already exists. This migration terminates without any changes.",
+      );
       return;
     }
 

+ 4 - 7
apps/app/src/migrations/20190619055421-adjust-page-grant.js

@@ -7,7 +7,6 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:migrate:adjust-page-grant');
 
 module.exports = {
-
   async up(db) {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
@@ -16,16 +15,14 @@ module.exports = {
 
     await Page.bulkWrite([
       {
-        updateMany:
-         {
-           filter: { grant: null },
-           update: { $set: { grant: Page.GRANT_PUBLIC } },
-         },
+        updateMany: {
+          filter: { grant: null },
+          update: { $set: { grant: Page.GRANT_PUBLIC } },
+        },
       },
     ]);
 
     logger.info('Migration has successfully applied');
-
   },
 
   down(db) {

+ 0 - 2
apps/app/src/migrations/20190624110950-fill-last-update-user.js

@@ -10,7 +10,6 @@ const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
  * FIX https://github.com/weseek/growi/issues/1067
  */
 module.exports = {
-
   async up(db) {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
@@ -47,5 +46,4 @@ module.exports = {
   down(db) {
     // do not rollback
   },
-
 };

+ 3 - 1
apps/app/src/migrations/20191102223901-drop-pages-indices.js

@@ -7,7 +7,9 @@ const logger = loggerFactory('growi:migrate:drop-pages-indices');
 
 async function dropIndexIfExists(db, collectionName, indexName) {
   // check existence of the collection
-  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  const items = await db
+    .listCollections({ name: collectionName }, { nameOnly: true })
+    .toArray();
   if (items.length === 0) {
     return;
   }

+ 0 - 2
apps/app/src/migrations/20191126173016-adjust-pages-path.js

@@ -7,7 +7,6 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:adjust-pages-path');
 
-
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
@@ -18,7 +17,6 @@ module.exports = {
     // retrieve target data
     const pages = await Page.find({ path: /^(?!\/)/ });
 
-
     // create requests for bulkWrite
     const requests = pages.map((page) => {
       const adjustedPath = addHeadingSlash(page.path);

+ 6 - 2
apps/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -3,11 +3,15 @@ import mongoose from 'mongoose';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:migrate:drop-wrong-index-of-page-tag-relation');
+const logger = loggerFactory(
+  'growi:migrate:drop-wrong-index-of-page-tag-relation',
+);
 
 async function dropIndexIfExists(db, collectionName, indexName) {
   // check existence of the collection
-  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  const items = await db
+    .listCollections({ name: collectionName }, { nameOnly: true })
+    .toArray();
   if (items.length === 0) {
     return;
   }

+ 6 - 4
apps/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js

@@ -5,8 +5,9 @@ import UserGroupRelation from '~/server/models/user-group-relation';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgroup');
+const logger = loggerFactory(
+  'growi:migrate:remove-deleteduser-from-relationgroup',
+);
 
 module.exports = {
   async up(db) {
@@ -16,13 +17,14 @@ module.exports = {
     const User = userModelFactory();
 
     const deletedUsers = await User.find({ status: 4 }); // deleted user
-    const requests = await UserGroupRelation.remove({ relatedUser: deletedUsers });
+    const requests = await UserGroupRelation.remove({
+      relatedUser: deletedUsers,
+    });
 
     if (requests.size === 0) {
       return logger.info('This migration terminates without any changes.');
     }
     logger.info('Migration has successfully applied');
-
   },
 
   down(db, next) {

+ 8 - 2
apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -13,8 +13,14 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Promise.all([
-      await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark
-      await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('blue-night') }, { value: JSON.stringify('mono-blue') }), // update blue-night
+      await Config.findOneAndUpdate(
+        { key: 'customize:theme', value: JSON.stringify('default-dark') },
+        { value: JSON.stringify('default') },
+      ), // update default-dark
+      await Config.findOneAndUpdate(
+        { key: 'customize:theme', value: JSON.stringify('blue-night') },
+        { value: JSON.stringify('mono-blue') },
+      ), // update blue-night
     ]);
 
     logger.info('Migration has successfully applied');

+ 2 - 8
apps/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -27,15 +27,9 @@ module.exports = {
       ),
 
       // update en-US -> en_US
-      User.updateMany(
-        { lang: 'en-US' },
-        { lang: 'en_US' },
-      ),
+      User.updateMany({ lang: 'en-US' }, { lang: 'en_US' }),
       // update ja -> ja_JP
-      User.updateMany(
-        { lang: 'ja' },
-        { lang: 'ja_JP' },
-      ),
+      User.updateMany({ lang: 'ja' }, { lang: 'ja_JP' }),
     ]);
 
     logger.info('Migration has successfully applied');

+ 1 - 1
apps/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -42,7 +42,7 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     const theme = await Config.findOne({ key: 'customize:theme' });
-    const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
+    const insertLayoutType = theme.value === '"kibela"' ? 'kibela' : 'growi';
 
     const insertConfig = new Config({
       key: 'customize:layout',

+ 3 - 1
apps/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -56,7 +56,9 @@ module.exports = {
     logger.info('Rollback migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
+    await Config.deleteMany({
+      key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] },
+    });
 
     logger.info('Migration has been successfully rollbacked');
   },

+ 10 - 6
apps/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -20,15 +20,20 @@ module.exports = {
     });
 
     if (sesAccessKeyId == null) {
-      return logger.info('The key \'mail:sesAccessKeyId\' does not exist, value of transmission method will be set smtp automatically.');
+      return logger.info(
+        "The key 'mail:sesAccessKeyId' does not exist, value of transmission method will be set smtp automatically.",
+      );
     }
     if (transmissionMethod != null) {
-      return logger.info('The key \'mail:transmissionMethod\' already exists, there is no need to migrate.');
+      return logger.info(
+        "The key 'mail:transmissionMethod' already exists, there is no need to migrate.",
+      );
     }
 
-    const value = sesAccessKeyId.value != null
-      ? JSON.stringify('ses')
-      : JSON.stringify('smtp');
+    const value =
+      sesAccessKeyId.value != null
+        ? JSON.stringify('ses')
+        : JSON.stringify('smtp');
 
     await Config.create({
       ns: 'crowi',
@@ -36,7 +41,6 @@ module.exports = {
       value,
     });
     logger.info('Migration has successfully applied');
-
   },
 
   async down(db, client) {

+ 0 - 1
apps/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -3,7 +3,6 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 const mongoose = require('mongoose');

+ 2 - 3
apps/app/src/migrations/20200915035234-rename-s3-config.js

@@ -3,7 +3,6 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 const mongoose = require('mongoose');
@@ -40,7 +39,7 @@ module.exports = {
       return {
         updateOne: {
           filter: { key: awsConfig.oldValue },
-          update:  { key: awsConfig.newValue },
+          update: { key: awsConfig.newValue },
         },
       };
     });
@@ -59,7 +58,7 @@ module.exports = {
       return {
         updateOne: {
           filter: { key: awsConfig.newValue },
-          update:  { key: awsConfig.oldValue },
+          update: { key: awsConfig.oldValue },
         },
       };
     });

+ 5 - 2
apps/app/src/migrations/20210420160380-convert-double-to-date.js

@@ -1,7 +1,11 @@
 import mongoose from 'mongoose';
 
 import getPageModel from '~/server/models/page';
-import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import {
+  getModelSafely,
+  getMongoUri,
+  mongoOptions,
+} from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
@@ -31,7 +35,6 @@ module.exports = {
     await Page.bulkWrite(operations);
 
     logger.info('Migration has successfully applied');
-
   },
 
   down(db) {

+ 8 - 4
apps/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js

@@ -4,8 +4,9 @@ import slackAppIntegrationFactory from '~/server/models/slack-app-integration';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-value');
+const logger = loggerFactory(
+  'growi:migrate:slack-app-integration-set-default-value',
+);
 
 module.exports = {
   async up(db) {
@@ -16,8 +17,11 @@ module.exports = {
 
     // Add togetter command if supportedCommandsForBroadcastUse already exists
     const slackAppIntegrations = await SlackAppIntegration.find();
-    slackAppIntegrations.forEach(async(doc) => {
-      if (doc.supportedCommandsForSingleUse != null && !doc.supportedCommandsForSingleUse.includes('togetter')) {
+    slackAppIntegrations.forEach(async (doc) => {
+      if (
+        doc.supportedCommandsForSingleUse != null &&
+        !doc.supportedCommandsForSingleUse.includes('togetter')
+      ) {
         doc.supportedCommandsForSingleUse.push('togetter');
       }
       await doc.save();

+ 7 - 3
apps/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js

@@ -1,12 +1,16 @@
-import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
+import {
+  defaultSupportedCommandsNameForBroadcastUse,
+  defaultSupportedCommandsNameForSingleUse,
+} from '@growi/slack';
 import mongoose from 'mongoose';
 
 import slackAppIntegrationFactory from '~/server/models/slack-app-integration';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:migrate-slack-app-integration-schema');
+const logger = loggerFactory(
+  'growi:migrate:migrate-slack-app-integration-schema',
+);
 
 // create default data
 const defaultDataForBroadcastUse = {};

+ 22 - 19
apps/app/src/migrations/20210921173042-add-is-trashed-field.js

@@ -1,7 +1,11 @@
 import mongoose from 'mongoose';
 
 import getPageModel from '~/server/models/page';
-import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import {
+  getModelSafely,
+  getMongoUri,
+  mongoOptions,
+} from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:add-column-is-trashed');
@@ -11,11 +15,13 @@ const LIMIT = 1000;
 /**
  * set isPageTrashed of pagetagrelations included in updateIdList as true
  */
-const updateIsPageTrashed = async(db, updateIdList) => {
-  await db.collection('pagetagrelations').updateMany(
-    { relatedPage: { $in: updateIdList } },
-    { $set: { isPageTrashed: true } },
-  );
+const updateIsPageTrashed = async (db, updateIdList) => {
+  await db
+    .collection('pagetagrelations')
+    .updateMany(
+      { relatedPage: { $in: updateIdList } },
+      { $set: { isPageTrashed: true } },
+    );
 };
 
 module.exports = {
@@ -27,12 +33,13 @@ module.exports = {
     let updateDeletedPageIds = [];
 
     // set isPageTrashed as false temporarily
-    await db.collection('pagetagrelations').updateMany(
-      {},
-      { $set: { isPageTrashed: false } },
-    );
+    await db
+      .collection('pagetagrelations')
+      .updateMany({}, { $set: { isPageTrashed: false } });
 
-    for await (const deletedPage of Page.find({ status: Page.STATUS_DELETED }).select('_id').cursor()) {
+    for await (const deletedPage of Page.find({ status: Page.STATUS_DELETED })
+      .select('_id')
+      .cursor()) {
       updateDeletedPageIds.push(deletedPage._id);
       // excute updateMany by one thousand ids
       if (updateDeletedPageIds.length === LIMIT) {
@@ -47,7 +54,6 @@ module.exports = {
     }
 
     logger.info('Migration has successfully applied');
-
   },
 
   async down(db) {
@@ -55,16 +61,13 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
-      await db.collection('pagetagrelations').updateMany(
-        {},
-        { $unset: { isPageTrashed: '' } },
-      );
+      await db
+        .collection('pagetagrelations')
+        .updateMany({}, { $unset: { isPageTrashed: '' } });
       logger.info('Migration has been successfully rollbacked');
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       logger.info('Migration has failed');
     }
-
   },
 };

+ 12 - 5
apps/app/src/migrations/20211005120030-slack-app-integration-rename-keys.js

@@ -18,9 +18,13 @@ module.exports = {
 
     // create operations
     const operations = slackAppIntegrations.map((doc) => {
-      const permissionsForSingleUseCommands = doc._doc.permissionsForSingleUseCommands;
+      const permissionsForSingleUseCommands =
+        doc._doc.permissionsForSingleUseCommands;
       const createValue = permissionsForSingleUseCommands.get('create', false);
-      const togetterValue = permissionsForSingleUseCommands.get('togetter', false);
+      const togetterValue = permissionsForSingleUseCommands.get(
+        'togetter',
+        false,
+      );
 
       const newPermissionsForSingleUseCommands = {
         note: createValue,
@@ -32,7 +36,8 @@ module.exports = {
           filter: { _id: doc._id },
           update: {
             $set: {
-              permissionsForSingleUseCommands: newPermissionsForSingleUseCommands,
+              permissionsForSingleUseCommands:
+                newPermissionsForSingleUseCommands,
             },
           },
         },
@@ -55,7 +60,8 @@ module.exports = {
 
     // create operations
     const operations = slackAppIntegrations.map((doc) => {
-      const permissionsForSingleUseCommands = doc._doc.permissionsForSingleUseCommands;
+      const permissionsForSingleUseCommands =
+        doc._doc.permissionsForSingleUseCommands;
       const noteValue = permissionsForSingleUseCommands.get('note', false);
       const keepValue = permissionsForSingleUseCommands.get('keep', false);
 
@@ -69,7 +75,8 @@ module.exports = {
           filter: { _id: doc._id },
           update: {
             $set: {
-              permissionsForSingleUseCommands: newPermissionsForSingleUseCommands,
+              permissionsForSingleUseCommands:
+                newPermissionsForSingleUseCommands,
             },
           },
         },

+ 22 - 7
apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js

@@ -5,19 +5,26 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:slack-app-integration-rename-keys');
 
 module.exports = {
   async up(db) {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    const isExist = (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) > 0;
+    const isExist =
+      (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) >
+      0;
     if (!isExist) return;
 
-    const commandPermissionValue = await Config.findOne({ key: 'slackbot:withoutProxy:commandPermission' });
+    const commandPermissionValue = await Config.findOne({
+      key: 'slackbot:withoutProxy:commandPermission',
+    });
     // do nothing if data is 'null' or null
-    if (commandPermissionValue._doc.value === 'null' || commandPermissionValue._doc.value == null) return;
+    if (
+      commandPermissionValue._doc.value === 'null' ||
+      commandPermissionValue._doc.value == null
+    )
+      return;
 
     const commandPermission = JSON.parse(commandPermissionValue._doc.value);
 
@@ -55,12 +62,20 @@ module.exports = {
   async down(db, next) {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    const isExist = (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) > 0;
+    const isExist =
+      (await Config.count({ key: 'slackbot:withoutProxy:commandPermission' })) >
+      0;
     if (!isExist) return next();
 
-    const commandPermissionValue = await Config.findOne({ key: 'slackbot:withoutProxy:commandPermission' });
+    const commandPermissionValue = await Config.findOne({
+      key: 'slackbot:withoutProxy:commandPermission',
+    });
     // do nothing if data is 'null' or null
-    if (commandPermissionValue._doc.value === 'null' || commandPermissionValue._doc.value == null) return next();
+    if (
+      commandPermissionValue._doc.value === 'null' ||
+      commandPermissionValue._doc.value == null
+    )
+      return next();
 
     const commandPermission = JSON.parse(commandPermissionValue._doc.value);
 

+ 19 - 10
apps/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js

@@ -5,8 +5,9 @@ import NamedQuery from '~/server/models/named-query';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:initialize-private-legacy-pages-named-query');
+const logger = loggerFactory(
+  'growi:migrate:initialize-private-legacy-pages-named-query',
+);
 
 module.exports = {
   async up(db, next) {
@@ -18,14 +19,18 @@ module.exports = {
         { delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES },
         { upsert: true },
       );
-    }
-    catch (err) {
-      logger.error('Failed to migrate named query for private legacy pages search delagator.', err);
+    } catch (err) {
+      logger.error(
+        'Failed to migrate named query for private legacy pages search delagator.',
+        err,
+      );
       throw err;
     }
 
     next();
-    logger.info('Successfully migrated named query for private legacy pages search delagator.');
+    logger.info(
+      'Successfully migrated named query for private legacy pages search delagator.',
+    );
   },
 
   async down(db, next) {
@@ -36,13 +41,17 @@ module.exports = {
         name: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
         delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
       });
-    }
-    catch (err) {
-      logger.error('Failed to delete named query for private legacy pages search delagator.', err);
+    } catch (err) {
+      logger.error(
+        'Failed to delete named query for private legacy pages search delagator.',
+        err,
+      );
       throw err;
     }
 
     next();
-    logger.info('Successfully deleted named query for private legacy pages search delagator.');
+    logger.info(
+      'Successfully deleted named query for private legacy pages search delagator.',
+    );
   },
 };

+ 19 - 16
apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js

@@ -1,16 +1,20 @@
+import mongoose from 'mongoose';
 import { Writable } from 'stream';
 import { pipeline } from 'stream/promises';
 
-import mongoose from 'mongoose';
-
 import getPageModel from '~/server/models/page';
 import { Revision } from '~/server/models/revision';
 import { createBatchStream } from '~/server/util/batch-stream';
-import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import {
+  getModelSafely,
+  getMongoUri,
+  mongoOptions,
+} from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:revision-path-to-page-id-schema-migration--fixed-7549');
+const logger = loggerFactory(
+  'growi:migrate:revision-path-to-page-id-schema-migration--fixed-7549',
+);
 
 const LIMIT = 300;
 
@@ -20,7 +24,10 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
 
-    const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
+    const pagesStream = await Page.find(
+      { revision: { $ne: null } },
+      { _id: 1, path: 1 },
+    ).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);
 
     const migratePagesStream = new Writable({
@@ -30,10 +37,7 @@ module.exports = {
           return {
             updateMany: {
               filter: {
-                $and: [
-                  { path: page.path },
-                  { pageId: { $exists: false } },
-                ],
+                $and: [{ path: page.path }, { pageId: { $exists: false } }],
               },
               update: [
                 {
@@ -66,7 +70,10 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
 
-    const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
+    const pagesStream = await Page.find(
+      { revision: { $ne: null } },
+      { _id: 1, path: 1 },
+    ).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);
 
     const migratePagesStream = new Writable({
@@ -76,11 +83,7 @@ module.exports = {
           return {
             updateMany: {
               filter: {
-                $and: [
-                  { pageId: page._id },
-                  { path: { $exists: false } },
-                ],
-
+                $and: [{ pageId: page._id }, { path: { $exists: false } }],
               },
               update: [
                 {

+ 16 - 8
apps/app/src/migrations/20220131001218-convert-redirect-to-pages-to-page-redirect-documents.js

@@ -3,21 +3,31 @@ import mongoose from 'mongoose';
 // eslint-disable-next-line import/no-named-as-default
 import PageRedirectModel from '~/server/models/page-redirect';
 import { createBatchStream } from '~/server/util/batch-stream';
-import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import {
+  getModelSafely,
+  getMongoUri,
+  mongoOptions,
+} from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:migrate:convert-redirect-to-pages-to-page-redirect-documents');
+const logger = loggerFactory(
+  'growi:migrate:convert-redirect-to-pages-to-page-redirect-documents',
+);
 
 const BATCH_SIZE = 100;
 
-
 module.exports = {
   async up(db, client) {
     await mongoose.connect(getMongoUri(), mongoOptions);
     const pageCollection = await db.collection('pages');
     const PageRedirect = getModelSafely('PageRedirect') || PageRedirectModel;
 
-    const cursor = pageCollection.find({ redirectTo: { $exists: true, $ne: null } }, { path: 1, redirectTo: 1, _id: 0 }).stream();
+    const cursor = pageCollection
+      .find(
+        { redirectTo: { $exists: true, $ne: null } },
+        { path: 1, redirectTo: 1, _id: 0 },
+      )
+      .stream();
     const batchStream = createBatchStream(BATCH_SIZE);
 
     // redirectTo => PageRedirect
@@ -35,8 +45,7 @@ module.exports = {
 
       try {
         await PageRedirect.bulkWrite(insertPageRedirectOperations);
-      }
-      catch (err) {
+      } catch (err) {
         if (err.code !== 11000) {
           throw Error(`Failed to migrate: ${err}`);
         }
@@ -71,8 +80,7 @@ module.exports = {
 
       try {
         await pageCollection.bulkWrite(insertPageOperations);
-      }
-      catch (err) {
+      } catch (err) {
         if (err.code !== 11000) {
           throw Error(`Failed to migrate: ${err}`);
         }

+ 21 - 24
apps/app/src/migrations/20220311011114-convert-page-delete-config.js

@@ -1,7 +1,8 @@
 import mongoose from 'mongoose';
 
 import {
-  PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
+  PageRecursiveDeleteCompConfigValue,
+  PageRecursiveDeleteConfigValue,
 } from '~/interfaces/page-delete-config';
 import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
@@ -9,14 +10,14 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:convert-page-delete-config');
 
-
 module.exports = {
   async up(db, client) {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    const isNewConfigExists = await Config.count({
-      key: 'security:pageDeletionAuthority',
-    }) > 0;
+    const isNewConfigExists =
+      (await Config.count({
+        key: 'security:pageDeletionAuthority',
+      })) > 0;
 
     if (isNewConfigExists) {
       logger.info('This migration is skipped because new configs are existed.');
@@ -31,25 +32,21 @@ module.exports = {
     const oldValue = oldConfig?.value ?? '"anyOne"';
 
     try {
-
-      await Config.insertMany(
-        [
-          {
-            key: 'security:pageDeletionAuthority',
-            value: oldValue,
-          },
-          {
-            key: 'security:pageRecursiveDeletionAuthority',
-            value: `"${PageRecursiveDeleteConfigValue.Inherit}"`,
-          },
-          {
-            key: 'security:pageRecursiveCompleteDeletionAuthority',
-            value: `"${PageRecursiveDeleteCompConfigValue.Inherit}"`,
-          },
-        ],
-      );
-    }
-    catch (err) {
+      await Config.insertMany([
+        {
+          key: 'security:pageDeletionAuthority',
+          value: oldValue,
+        },
+        {
+          key: 'security:pageRecursiveDeletionAuthority',
+          value: `"${PageRecursiveDeleteConfigValue.Inherit}"`,
+        },
+        {
+          key: 'security:pageRecursiveCompleteDeletionAuthority',
+          value: `"${PageRecursiveDeleteCompConfigValue.Inherit}"`,
+        },
+      ]);
+    } catch (err) {
       logger.error('Failed to migrate page delete configs', err);
       throw err;
     }

+ 3 - 1
apps/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js

@@ -4,7 +4,9 @@ import userModelFactory from '~/server/models/user';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:migrate:set-sparse-option-to-slack-member-id');
+const logger = loggerFactory(
+  'growi:migrate:set-sparse-option-to-slack-member-id',
+);
 
 /**
  * set sparse option to slackMemberId

+ 15 - 13
apps/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js

@@ -5,7 +5,9 @@ import { Attachment } from '~/server/models/attachment';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:migrate:add-attachment-type-to-existing-attachments');
+const logger = loggerFactory(
+  'growi:migrate:add-attachment-type-to-existing-attachments',
+);
 
 module.exports = {
   async up(db) {
@@ -15,26 +17,26 @@ module.exports = {
     // Add attachmentType for wiki page
     // Filter pages where "attachmentType" doesn't exist and "page" is not null
     const operationsForWikiPage = {
-      updateMany:
-         {
-           filter: { page: { $ne: null }, attachmentType: { $exists: false } },
-           update: { $set: { attachmentType: AttachmentType.WIKI_PAGE } },
-         },
+      updateMany: {
+        filter: { page: { $ne: null }, attachmentType: { $exists: false } },
+        update: { $set: { attachmentType: AttachmentType.WIKI_PAGE } },
+      },
     };
 
     // Add attachmentType for profile image
     // Filter pages where "attachmentType" doesn't exist and "page" is null
     const operationsForProfileImage = {
-      updateMany:
-        {
-          filter: { page: { $eq: null }, attachmentType: { $exists: false } },
-          update: { $set: { attachmentType: AttachmentType.PROFILE_IMAGE } },
-        },
+      updateMany: {
+        filter: { page: { $eq: null }, attachmentType: { $exists: false } },
+        update: { $set: { attachmentType: AttachmentType.PROFILE_IMAGE } },
+      },
     };
-    await Attachment.bulkWrite([operationsForWikiPage, operationsForProfileImage]);
+    await Attachment.bulkWrite([
+      operationsForWikiPage,
+      operationsForProfileImage,
+    ]);
 
     logger.info('Migration has successfully applied');
-
   },
 
   async down(db) {

+ 3 - 2
apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js

@@ -3,7 +3,6 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:remove-isSavedStatesOfTabChanges');
 
 const mongoose = require('mongoose');
@@ -13,7 +12,9 @@ module.exports = {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    await Config.findOneAndDelete({ key: 'customize:isSavedStatesOfTabChanges' }); // remove isSavedStatesOfTabChanges
+    await Config.findOneAndDelete({
+      key: 'customize:isSavedStatesOfTabChanges',
+    }); // remove isSavedStatesOfTabChanges
 
     logger.info('Migration has successfully applied');
   },

+ 3 - 2
apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -3,7 +3,6 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:remove-basic-auth-related-config');
 
 const mongoose = require('mongoose');
@@ -14,7 +13,9 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     await Config.findOneAndDelete({ key: 'security:passport-basic:isEnabled' });
-    await Config.findOneAndDelete({ key: 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser' });
+    await Config.findOneAndDelete({
+      key: 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser',
+    });
 
     logger.info('Migration has successfully applied');
   },

+ 9 - 4
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -3,8 +3,9 @@ import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:migrate:remove-presentation-configurations');
+const logger = loggerFactory(
+  'growi:migrate:remove-presentation-configurations',
+);
 
 const mongoose = require('mongoose');
 
@@ -13,8 +14,12 @@ module.exports = {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
 
-    await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakSeparator' });
-    await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakCustomSeparator' });
+    await Config.findOneAndDelete({
+      key: 'markdown:presentation:pageBreakSeparator',
+    });
+    await Config.findOneAndDelete({
+      key: 'markdown:presentation:pageBreakCustomSeparator',
+    });
 
     logger.info('Migration has successfully applied');
   },

+ 51 - 42
apps/app/src/migrations/20230723061824-granted-group-to-array-of-objects.js

@@ -10,21 +10,18 @@ module.exports = {
     const pageOperationCollection = await db.collection('pageoperations');
     // Convert grantedGroup to array
     // Set the model type of grantedGroup to UserGroup for Pages that were created before ExternalUserGroup was introduced
-    await pageCollection.updateMany(
-      { grantedGroup: { $ne: null } },
-      [
-        {
-          $set: {
-            grantedGroup: [
-              {
-                type: 'UserGroup',
-                item: '$grantedGroup',
-              },
-            ],
-          },
+    await pageCollection.updateMany({ grantedGroup: { $ne: null } }, [
+      {
+        $set: {
+          grantedGroup: [
+            {
+              type: 'UserGroup',
+              item: '$grantedGroup',
+            },
+          ],
         },
-      ],
-    );
+      },
+    ]);
 
     await pageOperationCollection.updateMany(
       { 'options.grantUserGroupId': { $ne: null } },
@@ -75,18 +72,24 @@ module.exports = {
     );
 
     // rename fields
-    await pageCollection.updateMany({}, {
-      $rename: {
-        grantedGroup: 'grantedGroups',
+    await pageCollection.updateMany(
+      {},
+      {
+        $rename: {
+          grantedGroup: 'grantedGroups',
+        },
       },
-    });
-    await pageOperationCollection.updateMany({}, {
-      $rename: {
-        'options.grantUserGroupId': 'options.grantUserGroupIds',
-        'page.grantedGroup': 'page.grantedGroups',
-        'exPage.grantedGroup': 'exPage.grantedGroups',
+    );
+    await pageOperationCollection.updateMany(
+      {},
+      {
+        $rename: {
+          'options.grantUserGroupId': 'options.grantUserGroupIds',
+          'page.grantedGroup': 'page.grantedGroups',
+          'exPage.grantedGroup': 'exPage.grantedGroups',
+        },
       },
-    });
+    );
 
     logger.info('Migration has successfully applied');
   },
@@ -97,22 +100,21 @@ module.exports = {
     const pageCollection = await db.collection('pages');
     const pageOperationCollection = await db.collection('pageoperations');
 
-    await pageCollection.updateMany(
-      { grantedGroups: { $exists: true } },
-      [
-        {
-          $set: {
-            grantedGroups: { $arrayElemAt: ['$grantedGroups.item', 0] },
-          },
+    await pageCollection.updateMany({ grantedGroups: { $exists: true } }, [
+      {
+        $set: {
+          grantedGroups: { $arrayElemAt: ['$grantedGroups.item', 0] },
         },
-      ],
-    );
+      },
+    ]);
     await pageOperationCollection.updateMany(
       { 'options.grantUserGroupIds': { $exists: true } },
       [
         {
           $set: {
-            'options.grantUserGroupIds': { $arrayElemAt: ['options.grantUserGroupIds.item', 0] },
+            'options.grantUserGroupIds': {
+              $arrayElemAt: ['options.grantUserGroupIds.item', 0],
+            },
           },
         },
       ],
@@ -122,7 +124,9 @@ module.exports = {
       [
         {
           $set: {
-            'page.grantedGroups': { $arrayElemAt: ['page.grantedGroups.item', 0] },
+            'page.grantedGroups': {
+              $arrayElemAt: ['page.grantedGroups.item', 0],
+            },
           },
         },
       ],
@@ -132,7 +136,9 @@ module.exports = {
       [
         {
           $set: {
-            'exPage.grantedGroups': { $arrayElemAt: ['exPage.grantedGroups.item', 0] },
+            'exPage.grantedGroups': {
+              $arrayElemAt: ['exPage.grantedGroups.item', 0],
+            },
           },
         },
       ],
@@ -147,13 +153,16 @@ module.exports = {
         },
       },
     );
-    await pageOperationCollection.updateMany({}, {
-      $rename: {
-        'options.grantUserGroupIds': 'options.grantUserGroupId',
-        'page.grantedGroups': 'page.grantedGroup',
-        'exPage.grantedGroups': 'exPage.grantedGroup',
+    await pageOperationCollection.updateMany(
+      {},
+      {
+        $rename: {
+          'options.grantUserGroupIds': 'options.grantUserGroupId',
+          'page.grantedGroups': 'page.grantedGroup',
+          'exPage.grantedGroups': 'exPage.grantedGroup',
+        },
       },
-    });
+    );
 
     logger.info('Migration has been successfully rollbacked');
   },

+ 18 - 11
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -23,11 +23,15 @@ module.exports = {
       // Set app:installed date.
       // refs: https://mongoosejs.com/docs/6.x/docs/timestamps.html#disabling-timestamps
       //       Read the section after "Disabling timestamps also lets you set timestamps yourself..."
-      const updatedConfig = await Config.findOneAndUpdate({ _id: appInstalled._id }, { createdAt: initialUserCreatedAt }, {
-        new: true,
-        timestamps: false,
-        strict: false,
-      });
+      const updatedConfig = await Config.findOneAndUpdate(
+        { _id: appInstalled._id },
+        { createdAt: initialUserCreatedAt },
+        {
+          new: true,
+          timestamps: false,
+          strict: false,
+        },
+      );
       logger.debug('updatedConfig: ', updatedConfig);
     }
 
@@ -40,13 +44,16 @@ module.exports = {
 
     const appInstalled = await Config.findOne({ key: 'app:installed' });
     if (appInstalled != null) {
-
       // Unset app:installed date.
-      const updatedConfig = await Config.findOneAndUpdate({ _id: appInstalled._id }, { $unset: { createdAt: 1 } }, {
-        new: true,
-        timestamps: false,
-        strict: false,
-      });
+      const updatedConfig = await Config.findOneAndUpdate(
+        { _id: appInstalled._id },
+        { $unset: { createdAt: 1 } },
+        {
+          new: true,
+          timestamps: false,
+          strict: false,
+        },
+      );
       logger.debug('updatedConfig: ', updatedConfig);
     }
 

+ 0 - 1
apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js

@@ -3,7 +3,6 @@ import UserUISettings from '~/server/models/user-ui-settings';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:clean-user-ui-settings-collection');
 
 const mongoose = require('mongoose');

+ 6 - 9
apps/app/src/migrations/20231223155127-non-null-granted-groups.js

@@ -8,16 +8,13 @@ module.exports = {
 
     const pageCollection = await db.collection('pages');
 
-    await pageCollection.updateMany(
-      { grantedGroups: { $eq: null } },
-      [
-        {
-          $set: {
-            grantedGroups: [],
-          },
+    await pageCollection.updateMany({ grantedGroups: { $eq: null } }, [
+      {
+        $set: {
+          grantedGroups: [],
         },
-      ],
-    );
+      },
+    ]);
 
     logger.info('Migration has successfully applied');
   },

+ 9 - 10
apps/app/src/migrations/20240924181317-changed-status-in-inappnotifications-from-unread-to-unopened.js

@@ -1,22 +1,21 @@
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:changed-status-in-inappnotifications-from-unread-to-unopened');
+const logger = loggerFactory(
+  'growi:changed-status-in-inappnotifications-from-unread-to-unopened',
+);
 
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
 
     const unreadInAppnotifications = await db.collection('inappnotifications');
-    await unreadInAppnotifications.updateMany(
-      { status: { $eq: 'UNREAD' } },
-      [
-        {
-          $set: {
-            status: 'UNOPENED',
-          },
+    await unreadInAppnotifications.updateMany({ status: { $eq: 'UNREAD' } }, [
+      {
+        $set: {
+          status: 'UNOPENED',
         },
-      ],
-    );
+      },
+    ]);
 
     logger.info('Migration has successfully applied');
   },

+ 19 - 12
apps/app/src/migrations/20241107172359-rename-pageId-to-page.js

@@ -4,12 +4,13 @@ import VectorStoreFileRelationModel from '~/features/openai/server/models/vector
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:migrate:rename-pageId-to-page');
 
 async function dropIndexIfExists(db, collectionName, indexName) {
   // check existence of the collection
-  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  const items = await db
+    .listCollections({ name: collectionName }, { nameOnly: true })
+    .toArray();
   if (items.length === 0) {
     return;
   }
@@ -26,20 +27,26 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     // Drop index
-    await dropIndexIfExists(db, 'vectorstorefilerelations', 'vectorStoreRelationId_1_pageId_1');
+    await dropIndexIfExists(
+      db,
+      'vectorstorefilerelations',
+      'vectorStoreRelationId_1_pageId_1',
+    );
 
     // Rename field (pageId -> page)
-    await VectorStoreFileRelationModel.updateMany(
-      {},
-      [
-        { $set: { page: '$pageId' } },
-        { $unset: ['pageId'] },
-      ],
-    );
+    await VectorStoreFileRelationModel.updateMany({}, [
+      { $set: { page: '$pageId' } },
+      { $unset: ['pageId'] },
+    ]);
 
     // Create index
-    const collection = mongoose.connection.collection('vectorstorefilerelations');
-    await collection.createIndex({ vectorStoreRelationId: 1, page: 1 }, { unique: true });
+    const collection = mongoose.connection.collection(
+      'vectorstorefilerelations',
+    );
+    await collection.createIndex(
+      { vectorStoreRelationId: 1, page: 1 },
+      { unique: true },
+    );
   },
 
   async down() {

+ 30 - 9
apps/app/src/migrations/20250522105040-delete-old-index-for-vector-store-file-relation.js

@@ -3,11 +3,15 @@ import mongoose from 'mongoose';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
-const logger = loggerFactory('growi:migrate:vector-store-file-relation-index-migration');
+const logger = loggerFactory(
+  'growi:migrate:vector-store-file-relation-index-migration',
+);
 
 async function dropIndexIfExists(db, collectionName, indexName) {
   // check existence of the collection
-  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  const items = await db
+    .listCollections({ name: collectionName }, { nameOnly: true })
+    .toArray();
   if (items.length === 0) {
     return;
   }
@@ -25,12 +29,20 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     // Drop old index
-    await dropIndexIfExists(db, 'vectorstorefilerelations', 'vectorStoreRelationId_1_page_1');
+    await dropIndexIfExists(
+      db,
+      'vectorstorefilerelations',
+      'vectorStoreRelationId_1_page_1',
+    );
 
     // Create index
-    const collection = mongoose.connection.collection('vectorstorefilerelations');
-    await collection.createIndex({ vectorStoreRelationId: 1, page: 1, attachment: 1 }, { unique: true });
-
+    const collection = mongoose.connection.collection(
+      'vectorstorefilerelations',
+    );
+    await collection.createIndex(
+      { vectorStoreRelationId: 1, page: 1, attachment: 1 },
+      { unique: true },
+    );
   },
   async down(db) {
     logger.info('Rollback migration');
@@ -38,10 +50,19 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
 
     // Drop new index
-    await dropIndexIfExists(db, 'vectorstorefilerelations', 'vectorStoreRelationId_1_page_1_attachment_1');
+    await dropIndexIfExists(
+      db,
+      'vectorstorefilerelations',
+      'vectorStoreRelationId_1_page_1_attachment_1',
+    );
 
     // Recreate old index
-    const collection = mongoose.connection.collection('vectorstorefilerelations');
-    await collection.createIndex({ vectorStoreRelationId: 1, page: 1 }, { unique: true });
+    const collection = mongoose.connection.collection(
+      'vectorstorefilerelations',
+    );
+    await collection.createIndex(
+      { vectorStoreRelationId: 1, page: 1 },
+      { unique: true },
+    );
   },
 };

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

@@ -327,7 +327,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   async createIndex(index: string) {
     // TODO: https://redmine.weseek.co.jp/issues/168446
     if (isES7ClientDelegator(this.client)) {
-      const { mappings } = await import('^/resource/search/mappings-es7');
+      const { mappings } = await import('./mappings/mappings-es7');
       return this.client.indices.create({
         index,
         body: {
@@ -337,7 +337,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
 
     if (isES8ClientDelegator(this.client)) {
-      const { mappings } = await import('^/resource/search/mappings-es8');
+      const { mappings } = await import('./mappings/mappings-es8');
       return this.client.indices.create({
         index,
         ...mappings,
@@ -346,8 +346,8 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     if (isES9ClientDelegator(this.client)) {
       const { mappings } = process.env.CI == null
-        ? await import('^/resource/search/mappings-es9')
-        : await import('^/resource/search/mappings-es9-for-ci');
+        ? await import('./mappings/mappings-es9')
+        : await import('./mappings/mappings-es9-for-ci');
 
       return this.client.indices.create({
         index,

+ 0 - 0
apps/app/resource/search/mappings-es7.ts → apps/app/src/server/service/search-delegator/mappings/mappings-es7.ts


+ 0 - 0
apps/app/resource/search/mappings-es8.ts → apps/app/src/server/service/search-delegator/mappings/mappings-es8.ts


+ 0 - 0
apps/app/resource/search/mappings-es9-for-ci.ts → apps/app/src/server/service/search-delegator/mappings/mappings-es9-for-ci.ts


+ 0 - 0
apps/app/resource/search/mappings-es9.ts → apps/app/src/server/service/search-delegator/mappings/mappings-es9.ts


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

@@ -45,7 +45,7 @@
     "@tsed/schema": "=6.43.0",
     "@tsed/swagger": "=6.43.0",
     "@tsed/typeorm": "=6.43.0",
-    "axios": "^0.24.0",
+    "axios": "^1.11.0",
     "body-parser": "^1.20.3",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",

+ 12 - 1
biome.json

@@ -25,7 +25,18 @@
       "!packages/pdf-converter-client/src/index.ts",
       "!apps/app/playwright/**",
       "!apps/app/public/**",
-      "!apps/app/src/**",
+      "!apps/app/src/client/**",
+      "!apps/app/src/components/**",
+      "!apps/app/src/features/**",
+      "!apps/app/src/interfaces/**",
+      "!apps/app/src/models/**",
+      "!apps/app/src/pages/**",
+      "!apps/app/src/server/**",
+      "!apps/app/src/services/**",
+      "!apps/app/src/stores/**",
+      "!apps/app/src/stores-universal/**",
+      "!apps/app/src/styles/**",
+      "!apps/app/src/utils/**",
       "!apps/app/test/integration/service/**",
       "!apps/app/test-with-vite/**"
     ]

+ 1 - 1
packages/slack/package.json

@@ -54,7 +54,7 @@
     "@types/bunyan": "^1.8.10",
     "@types/http-errors": "^2.0.3",
     "@types/url-join": "^4.0.2",
-    "axios": "^0.24.0",
+    "axios": "^1.11.0",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "crypto": "^1.0.1",

+ 99 - 97
pnpm-lock.yaml

@@ -1141,8 +1141,8 @@ importers:
         specifier: '=6.43.0'
         version: 6.43.0(typeorm@0.2.45(mysql2@2.3.3)(redis@3.1.2))
       axios:
-        specifier: ^0.24.0
-        version: 0.24.0
+        specifier: ^1.11.0
+        version: 1.11.0
       body-parser:
         specifier: ^1.20.3
         version: 1.20.3
@@ -1413,7 +1413,7 @@ importers:
     dependencies:
       axios:
         specifier: ^1.10.0
-        version: 1.10.0
+        version: 1.11.0
       tslib:
         specifier: ^2.8.0
         version: 2.8.1
@@ -1546,7 +1546,7 @@ importers:
         version: link:../ui
       axios:
         specifier: ^1.10.0
-        version: 1.10.0
+        version: 1.11.0
       bunyan:
         specifier: ^1.8.15
         version: 1.8.15
@@ -1790,7 +1790,7 @@ importers:
         version: 3.0.4
       axios:
         specifier: ^1.10.0
-        version: 1.10.0
+        version: 1.11.0
       hast-util-sanitize:
         specifier: ^5.0.1
         version: 5.0.1
@@ -1828,8 +1828,8 @@ importers:
         specifier: ^4.0.2
         version: 4.0.3
       axios:
-        specifier: ^0.24.0
-        version: 0.24.0
+        specifier: ^1.11.0
+        version: 1.11.0
       browser-bunyan:
         specifier: ^1.6.3
         version: 1.8.0
@@ -6467,11 +6467,8 @@ packages:
   axios@0.26.1:
     resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
 
-  axios@1.10.0:
-    resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
-
-  axios@1.9.0:
-    resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
+  axios@1.11.0:
+    resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
 
   axobject-query@2.2.0:
     resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==}
@@ -16324,7 +16321,7 @@ snapshots:
       '@azure/core-util': 1.10.0
       '@azure/logger': 1.1.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       tslib: 2.8.1
     transitivePeerDependencies:
       - supports-color
@@ -16426,7 +16423,7 @@ snapshots:
       '@babel/traverse': 7.24.6
       '@babel/types': 7.25.6
       convert-source-map: 2.0.0
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -16610,7 +16607,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.24.6
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -17162,7 +17159,7 @@ snapshots:
 
   '@elastic/elasticsearch@7.17.13':
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       hpagent: 0.1.2
       ms: 2.1.3
       secure-json-parse: 2.7.0
@@ -17190,7 +17187,7 @@ snapshots:
   '@elastic/transport@8.9.7':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       hpagent: 1.2.0
       ms: 2.1.3
       secure-json-parse: 3.0.2
@@ -17202,7 +17199,7 @@ snapshots:
   '@elastic/transport@9.0.2':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       hpagent: 1.2.0
       ms: 2.1.3
       secure-json-parse: 4.0.0
@@ -17388,7 +17385,7 @@ snapshots:
   '@eslint/eslintrc@2.0.3':
     dependencies:
       ajv: 6.12.6
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       espree: 9.6.1
       globals: 13.24.0
       ignore: 5.3.1
@@ -17473,7 +17470,7 @@ snapshots:
   '@humanwhocodes/config-array@0.11.8':
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -17506,7 +17503,7 @@ snapshots:
       '@antfu/install-pkg': 1.1.0
       '@antfu/utils': 8.1.1
       '@iconify/types': 2.0.0
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globals: 15.15.0
       kolorist: 1.8.0
       local-pkg: 1.1.1
@@ -18150,9 +18147,9 @@ snapshots:
 
   '@npmcli/agent@2.2.2':
     dependencies:
-      agent-base: 7.1.1(supports-color@10.0.0)
+      agent-base: 7.1.1
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       lru-cache: 10.4.3
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
@@ -18928,7 +18925,7 @@ snapshots:
       ajv: 8.17.1
       chalk: 4.1.2
       compare-versions: 6.1.1
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       esbuild: 0.24.2
       esutils: 2.0.3
       fs-extra: 11.2.0
@@ -19100,7 +19097,7 @@ snapshots:
 
   '@puppeteer/browsers@2.4.0':
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       extract-zip: 2.0.1
       progress: 2.0.3
       proxy-agent: 6.4.0
@@ -19314,7 +19311,7 @@ snapshots:
       '@slack/types': 2.14.0
       '@types/is-stream': 1.1.0
       '@types/node': 22.15.21
-      axios: 1.9.0
+      axios: 1.11.0
       eventemitter3: 3.1.2
       form-data: 2.5.5
       is-electron: 2.2.2
@@ -19330,7 +19327,7 @@ snapshots:
       '@slack/types': 2.14.0
       '@types/node': 22.15.21
       '@types/retry': 0.12.0
-      axios: 1.10.0
+      axios: 1.11.0
       eventemitter3: 5.0.1
       form-data: 4.0.4
       is-electron: 2.2.2
@@ -20136,7 +20133,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.15)
       colorette: 2.0.20
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       tslib: 2.8.1
@@ -20151,7 +20148,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.15)
       colorette: 2.0.20
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       tslib: 2.8.1
@@ -20315,7 +20312,7 @@ snapshots:
       '@types/fs-extra': 11.0.4
       '@types/inquirer': 9.0.7
       ajv: 8.17.1
-      axios: 1.9.0
+      axios: 1.11.0
       chalk: 5.3.0
       change-case: 5.4.4
       commander: 12.1.0
@@ -21271,7 +21268,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/type-utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       grapheme-splitter: 1.0.4
       ignore: 5.3.1
@@ -21290,7 +21287,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/type-utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       grapheme-splitter: 1.0.4
       ignore: 5.3.1
@@ -21308,7 +21305,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.0.4)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
     optionalDependencies:
       typescript: 5.0.4
@@ -21320,7 +21317,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.4.2)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
     optionalDependencies:
       typescript: 5.4.2
@@ -21337,7 +21334,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.0.4)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.0.4)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       tsutils: 3.21.0(typescript@5.0.4)
     optionalDependencies:
@@ -21349,7 +21346,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.4.2)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       tsutils: 3.21.0(typescript@5.4.2)
     optionalDependencies:
@@ -21364,7 +21361,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/visitor-keys': 5.59.7
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.6.3
@@ -21378,7 +21375,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/visitor-keys': 5.59.7
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.6.3
@@ -21491,7 +21488,7 @@ snapshots:
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 0.2.3
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.6
@@ -21556,7 +21553,7 @@ snapshots:
       sirv: 2.0.4
       tinyglobby: 0.2.6
       tinyrainbow: 1.2.0
-      vitest: 2.1.1(@types/node@20.14.0)(@vitest/ui@2.1.1)(happy-dom@15.7.4)(jsdom@26.1.0)(sass@1.77.6)(terser@5.43.1)
+      vitest: 2.1.1(@types/node@22.15.21)(@vitest/ui@2.1.1)(happy-dom@15.7.4)(jsdom@26.1.0)(sass@1.77.6)(terser@5.43.1)
 
   '@vitest/utils@2.1.1':
     dependencies:
@@ -21774,7 +21771,13 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  agent-base@7.1.1:
+    dependencies:
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -22132,17 +22135,9 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
-  axios@1.10.0:
+  axios@1.11.0:
     dependencies:
-      follow-redirects: 1.15.9(debug@4.4.1)
-      form-data: 4.0.4
-      proxy-from-env: 1.1.0
-    transitivePeerDependencies:
-      - debug
-
-  axios@1.9.0:
-    dependencies:
-      follow-redirects: 1.15.9(debug@4.4.1)
+      follow-redirects: 1.15.11
       form-data: 4.0.4
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -23050,7 +23045,7 @@ snapshots:
 
   connect-mongo@4.6.0(express-session@1.18.0)(mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       express-session: 1.18.0
       kruptein: 3.0.6
       mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.600.0)
@@ -24212,7 +24207,7 @@ snapshots:
 
   eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.26.0)(eslint@8.41.0):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.59.7(eslint@8.41.0)(typescript@5.0.4))(eslint-import-resolver-typescript@3.2.5)(eslint@8.41.0)
       glob: 7.2.3
@@ -24224,7 +24219,7 @@ snapshots:
 
   eslint-import-resolver-typescript@3.2.5(eslint-plugin-import@2.26.0)(eslint@8.41.0):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       enhanced-resolve: 5.18.2
       eslint: 8.41.0
       eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.59.7(eslint@8.41.0)(typescript@5.0.4))(eslint-import-resolver-typescript@3.2.5)(eslint@8.41.0)
@@ -24433,7 +24428,7 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.0
@@ -24639,7 +24634,7 @@ snapshots:
 
   extract-zip@2.0.1:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
@@ -24828,7 +24823,7 @@ snapshots:
 
   follow-redirects@1.15.9(debug@4.4.1):
     optionalDependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
 
   follow-redirects@1.5.10:
     dependencies:
@@ -24990,7 +24985,7 @@ snapshots:
   gaxios@6.7.1(encoding@0.1.13):
     dependencies:
       extend: 3.0.2
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       is-stream: 2.0.0
       node-fetch: 2.7.0(encoding@0.1.13)
       uuid: 9.0.1
@@ -25068,7 +25063,7 @@ snapshots:
     dependencies:
       basic-ftp: 5.0.5
       data-uri-to-buffer: 6.0.2
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       fs-extra: 11.2.0
     transitivePeerDependencies:
       - supports-color
@@ -25605,14 +25600,14 @@ snapshots:
     dependencies:
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
   http-proxy-agent@7.0.2:
     dependencies:
-      agent-base: 7.1.1(supports-color@10.0.0)
-      debug: 4.4.1(supports-color@10.0.0)
+      agent-base: 7.1.1
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -25635,7 +25630,14 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.5:
+    dependencies:
+      agent-base: 7.1.1
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -25649,7 +25651,7 @@ snapshots:
   https-proxy-agent@7.0.6:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -26091,7 +26093,7 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       source-map: 0.6.1
     transitivePeerDependencies:
@@ -26100,7 +26102,7 @@ snapshots:
   istanbul-lib-source-maps@5.0.6:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.29
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
       - supports-color
@@ -27618,7 +27620,7 @@ snapshots:
   micromark@4.0.0:
     dependencies:
       '@types/debug': 4.1.7
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.1
@@ -27807,10 +27809,10 @@ snapshots:
     dependencies:
       async-mutex: 0.4.1
       camelcase: 6.3.0
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       find-cache-dir: 3.3.2
       follow-redirects: 1.15.9(debug@4.4.1)
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       mongodb: 5.9.2(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))
       new-find-package-json: 2.0.0
       semver: 7.6.3
@@ -27915,7 +27917,7 @@ snapshots:
 
   mquery@4.0.3:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -28009,7 +28011,7 @@ snapshots:
 
   new-find-package-json@2.0.0:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -28569,11 +28571,11 @@ snapshots:
   pac-proxy-agent@7.0.2:
     dependencies:
       '@tootallnate/quickjs-emscripten': 0.23.0
-      agent-base: 7.1.1(supports-color@10.0.0)
-      debug: 4.4.1(supports-color@10.0.0)
+      agent-base: 7.1.1
+      debug: 4.4.1(supports-color@5.5.0)
       get-uri: 6.0.3
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       pac-resolver: 7.0.1
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
@@ -28708,7 +28710,7 @@ snapshots:
   passport-saml@3.2.4:
     dependencies:
       '@xmldom/xmldom': 0.7.13
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       passport-strategy: 1.0.0
       xml-crypto: 2.1.5
       xml-encryption: 2.0.0
@@ -29015,10 +29017,10 @@ snapshots:
 
   proxy-agent@6.4.0:
     dependencies:
-      agent-base: 7.1.1(supports-color@10.0.0)
-      debug: 4.4.1(supports-color@10.0.0)
+      agent-base: 7.1.1
+      debug: 4.4.1(supports-color@5.5.0)
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5(supports-color@10.0.0)
+      https-proxy-agent: 7.0.5
       lru-cache: 7.18.3
       pac-proxy-agent: 7.0.2
       proxy-from-env: 1.1.0
@@ -29064,7 +29066,7 @@ snapshots:
 
   puppeteer-cluster@0.24.0(puppeteer@23.6.1(typescript@5.4.2)):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       puppeteer: 23.6.1(typescript@5.4.2)
     transitivePeerDependencies:
       - supports-color
@@ -29073,7 +29075,7 @@ snapshots:
     dependencies:
       '@puppeteer/browsers': 2.4.0
       chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       devtools-protocol: 0.0.1354347
       typed-query-selector: 2.12.0
       ws: 8.18.0
@@ -29821,7 +29823,7 @@ snapshots:
 
   require-in-the-middle@7.4.0:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       module-details-from-path: 1.0.3
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -29876,7 +29878,7 @@ snapshots:
 
   retry-request@4.2.2:
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       extend: 3.0.2
     transitivePeerDependencies:
       - supports-color
@@ -30355,15 +30357,15 @@ snapshots:
   socks-proxy-agent@7.0.0:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       socks: 2.8.3
     transitivePeerDependencies:
       - supports-color
 
   socks-proxy-agent@8.0.4:
     dependencies:
-      agent-base: 7.1.1(supports-color@10.0.0)
-      debug: 4.4.1(supports-color@10.0.0)
+      agent-base: 7.1.1
+      debug: 4.4.1(supports-color@5.5.0)
       socks: 2.8.3
     transitivePeerDependencies:
       - supports-color
@@ -30521,7 +30523,7 @@ snapshots:
   streamroller@3.1.5:
     dependencies:
       date-format: 4.0.14
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       fs-extra: 8.1.0
     transitivePeerDependencies:
       - supports-color
@@ -30726,7 +30728,7 @@ snapshots:
       cosmiconfig: 9.0.0(typescript@5.0.4)
       css-functions-list: 3.2.2
       css-tree: 2.3.1
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       fast-glob: 3.3.2
       fastest-levenshtein: 1.0.16
       file-entry-cache: 8.0.0
@@ -30781,7 +30783,7 @@ snapshots:
     dependencies:
       component-emitter: 1.3.1
       cookiejar: 2.1.4
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       fast-safe-stringify: 2.1.1
       form-data: 4.0.4
       formidable: 3.5.4
@@ -31417,7 +31419,7 @@ snapshots:
       buffer: 6.0.3
       chalk: 4.1.2
       cli-highlight: 2.1.11
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       dotenv: 8.6.0
       glob: 7.2.3
       js-yaml: 4.1.0
@@ -31796,7 +31798,7 @@ snapshots:
   vite-node@2.1.1(@types/node@20.14.0)(sass@1.77.6)(terser@5.43.1):
     dependencies:
       cac: 6.7.14
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       pathe: 1.1.2
       vite: 5.4.19(@types/node@20.14.0)(sass@1.77.6)(terser@5.43.1)
     transitivePeerDependencies:
@@ -31813,7 +31815,7 @@ snapshots:
   vite-node@2.1.1(@types/node@22.15.21)(sass@1.77.6)(terser@5.43.1):
     dependencies:
       cac: 6.7.14
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       pathe: 1.1.2
       vite: 5.4.19(@types/node@22.15.21)(sass@1.77.6)(terser@5.43.1)
     transitivePeerDependencies:
@@ -31832,7 +31834,7 @@ snapshots:
       '@microsoft/api-extractor': 7.43.0(@types/node@20.14.0)
       '@rollup/pluginutils': 5.1.4(rollup@4.41.0)
       '@vue/language-core': 1.8.27(typescript@5.0.4)
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       kolorist: 1.8.0
       magic-string: 0.30.11
       typescript: 5.0.4
@@ -31846,7 +31848,7 @@ snapshots:
 
   vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.19(@types/node@20.14.0)(sass@1.77.6)(terser@5.43.1)):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globrex: 0.1.2
       tsconfck: 3.0.3(typescript@5.0.4)
     optionalDependencies:
@@ -31857,7 +31859,7 @@ snapshots:
 
   vite-tsconfig-paths@5.0.1(typescript@5.4.2)(vite@5.4.19(@types/node@22.15.21)(sass@1.77.6)(terser@5.43.1)):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       globrex: 0.1.2
       tsconfck: 3.0.3(typescript@5.4.2)
     optionalDependencies:
@@ -31904,7 +31906,7 @@ snapshots:
       '@vitest/spy': 2.1.1
       '@vitest/utils': 2.1.1
       chai: 5.1.1
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       magic-string: 0.30.11
       pathe: 1.1.2
       std-env: 3.7.0
@@ -31941,7 +31943,7 @@ snapshots:
       '@vitest/spy': 2.1.1
       '@vitest/utils': 2.1.1
       chai: 5.1.1
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       magic-string: 0.30.11
       pathe: 1.1.2
       std-env: 3.7.0
@@ -31989,7 +31991,7 @@ snapshots:
 
   vue-eslint-parser@7.11.0(eslint@8.41.0):
     dependencies:
-      debug: 4.4.1(supports-color@10.0.0)
+      debug: 4.4.1(supports-color@5.5.0)
       eslint: 8.41.0
       eslint-scope: 5.1.1
       eslint-visitor-keys: 1.3.0