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

Merge pull request #9176 from weseek/master

Release v7.0.21
mergify[bot] 1 год назад
Родитель
Сommit
3672d5d135
40 измененных файлов с 287 добавлено и 170 удалено
  1. 0 1
      .github/mergify.yml
  2. 12 5
      .github/workflows/ci-app-prod.yml
  3. 1 2
      apps/app/package.json
  4. 1 1
      apps/app/public/static/locales/en_US/commons.json
  5. 1 0
      apps/app/public/static/locales/en_US/translation.json
  6. 1 1
      apps/app/public/static/locales/fr_FR/commons.json
  7. 1 0
      apps/app/public/static/locales/fr_FR/translation.json
  8. 1 1
      apps/app/public/static/locales/ja_JP/commons.json
  9. 1 0
      apps/app/public/static/locales/ja_JP/translation.json
  10. 1 1
      apps/app/public/static/locales/zh_CN/commons.json
  11. 1 0
      apps/app/public/static/locales/zh_CN/translation.json
  12. 3 2
      apps/app/src/client/components/DescendantsPageList.tsx
  13. 3 1
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  14. 2 1
      apps/app/src/client/components/NotAvailable.tsx
  15. 3 1
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  16. 2 1
      apps/app/src/client/components/PageEditor/page-path-rename-utils.ts
  17. 23 7
      apps/app/src/client/components/SavePageControls.tsx
  18. 2 1
      apps/app/src/client/components/SearchPage/SearchPageBase.tsx
  19. 4 1
      apps/app/src/client/components/SearchPage/SearchResultContent.tsx
  20. 4 1
      apps/app/src/client/components/SearchPage/SearchResultList.tsx
  21. 2 1
      apps/app/src/client/components/Sidebar/PageTree/PageTreeSubstance.tsx
  22. 4 6
      apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  23. 3 2
      apps/app/src/client/components/TreeItem/NewPageInput/use-new-page-input.tsx
  24. 2 0
      apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx
  25. 3 0
      apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx
  26. 1 1
      apps/app/src/server/middlewares/apiv3-form-validator.ts
  27. 9 1
      apps/app/src/server/routes/apiv3/forgot-password.js
  28. 30 1
      apps/app/src/server/routes/apiv3/revisions.js
  29. 2 2
      apps/app/src/server/routes/forgot-password.ts
  30. 1 1
      apps/app/src/server/service/page/index.ts
  31. 34 13
      apps/app/src/stores/page-listing.tsx
  32. 1 1
      apps/slackbot-proxy/package.json
  33. 1 1
      package.json
  34. 12 12
      packages/editor/package.json
  35. 1 1
      packages/remark-attachment-refs/package.json
  36. 3 1
      packages/remark-lsx/package.json
  37. 39 2
      packages/remark-lsx/src/server/index.ts
  38. 8 5
      packages/remark-lsx/src/server/routes/list-pages/index.spec.ts
  39. 10 7
      packages/remark-lsx/src/server/routes/list-pages/index.ts
  40. 54 84
      yarn.lock

+ 0 - 1
.github/mergify.yml

@@ -25,7 +25,6 @@ pull_request_rules:
       - '#approved-reviews-by >= 1'
       - '#changes-requested-reviews-by = 0'
       - '#review-requested = 0'
-      - check-success = check-title
     actions:
       queue:
 

+ 12 - 5
.github/workflows/ci-app-prod.yml

@@ -19,11 +19,6 @@ on:
       - '!apps/app/docker/**'
       - packages/**
   pull_request:
-    branches:
-      - master
-      - dev/7.*.x
-      - dev/6.*.x
-      - release/*
     types: [opened, reopened, synchronize]
     paths:
       - .github/mergify.yml
@@ -47,6 +42,12 @@ jobs:
 
   test-prod-node18:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
       node-version: 18.x
       skip-e2e-test: true
@@ -56,6 +57,12 @@ jobs:
 
   test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
       node-version: 20.x
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}

+ 1 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.20",
+  "version": "7.0.21-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -265,7 +265,6 @@
     "null-loader": "^4.0.1",
     "plantuml-encoder": "^1.2.5",
     "pretty-bytes": "^6.1.1",
-    "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",

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

@@ -157,6 +157,6 @@
     "publish_transfer_key": "Publish transfer key",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",
     "once_transfer_key_used": "Once the transfer key is used for transfer, it cannot be used for any other transfer.",
-    "transfer_to_growi_cloud": "For more details, please click <a href='{{documentationUrl}}/ja/admin-guide/management-cookbook/g2g-transfer.html'>here.</a>"
+    "transfer_to_growi_cloud": "For more details, please click <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>here.</a>"
   }
 }

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

@@ -64,6 +64,7 @@
   "Presentation Mode": "Presentation",
   "Not available for guest": "Not available for guest",
   "Not available in this version": "Not available in this version",
+  "Not available when \"anyone with the link\" is selected": "If \"anyone with the link\" is selected, the scope cannot be overridden.",
   "No users have liked this yet": "No users have liked this yet",
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have bookmarked yet": "No users have bookmarked yet",

+ 1 - 1
apps/app/public/static/locales/fr_FR/commons.json

@@ -157,6 +157,6 @@
     "publish_transfer_key": "Publier la clé de transfert",
     "transfer_key_limit": "Les clés de transfert sont valides durant une heure.",
     "once_transfer_key_used": "Les clés de transfert sont à usage unique.",
-    "transfer_to_growi_cloud": "Pour plus de détails, veuillez cliquer <a href='{{documentationUrl}}/ja/admin-guide/management-cookbook/g2g-transfer.html'>ici.</a>"
+    "transfer_to_growi_cloud": "Pour plus de détails, veuillez cliquer <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>ici.</a>"
   }
 }

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

@@ -64,6 +64,7 @@
   "Presentation Mode": "Mode présentation",
   "Not available for guest": "Indisponible pour les invités",
   "Not available in this version": "Indisponible dans cette version",
+  "Not available when \"anyone with the link\" is selected": "Si \"Tous les utilisateurs disposant du lien\" est sélectionné, la portée ne peut pas être modifiée",
   "No users have liked this yet": "Aucun utilisateur n'a aimé cette page",
   "No users have liked this yet.": "Aucun utilisateur n'a aimé cette page.",
   "No users have bookmarked yet": "Aucun utilisateur n'a mis en favoris cette page",

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

@@ -159,6 +159,6 @@
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
     "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
-    "transfer_to_growi_cloud": "※ 詳しくは <a href='{{documentationUrl}}/ja/admin-guide/management-cookbook/g2g-transfer.html'> GROWI お引越し機能</a>をご確認ください。"
+    "transfer_to_growi_cloud": "※ 詳しくは <a href='{{documentationUrl}}ja/admin-guide/management-cookbook/g2g-transfer.html'> GROWI お引越し機能</a>をご確認ください。"
   }
 }

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

@@ -61,6 +61,7 @@
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
+  "Not available when \"anyone with the link\" is selected": "「リンクを知っている人のみ」を選択している場合はスコープを上書きできません。",
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "Create Archive Page": "アーカイブページの作成",

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

@@ -160,6 +160,6 @@
     "publish_transfer_key": "发布迁移密钥",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",
     "once_transfer_key_used": "一旦迁移密钥被用于迁移,它将不再可用于进一步的迁移。",
-    "transfer_to_growi_cloud": "有关更多详情,请点击<a href='https://{{documentationUrl}}/ja/admin-guide/management-cookbook/g2g-transfer.html'>此处</a>。"
+    "transfer_to_growi_cloud": "有关更多详情,请点击<a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>此处</a>。"
   }
 }

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

@@ -61,6 +61,7 @@
   "Presentation Mode": "演示文稿",
   "Not available for guest": "不提供给客人",
   "Not available in this version": "此版本中不提供",
+  "Not available when \"anyone with the link\" is selected": "如果选择“任何人”,则无法覆盖范围",
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have bookmarked yet": "还没有用户加入书签",
   "Create Archive Page": "创建归档页",

+ 3 - 2
apps/app/src/client/components/DescendantsPageList.tsx

@@ -14,7 +14,7 @@ import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
 import {
   mutatePageTree,
-  useSWRxPageInfoForList, useSWRxPageList,
+  useSWRxPageInfoForList, useSWRxPageList, mutateRecentlyUpdated,
 } from '~/stores/page-listing';
 
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
@@ -67,7 +67,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
     else {
       toastSuccess(t('deleted_pages_completely', { path }));
     }
-
+    mutateRecentlyUpdated();
     mutatePageTree();
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
@@ -77,6 +77,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
 
+    mutateRecentlyUpdated();
     mutatePageTree();
     if (onPagePutBacked != null) {
       onPagePutBacked(path);

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

@@ -34,7 +34,7 @@ import {
 import {
   useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
 } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import {
   useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
@@ -271,6 +271,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     openRenameModal(page, { onRenamed: renamedHandler });
   }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
@@ -294,6 +295,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);

+ 2 - 1
apps/app/src/client/components/NotAvailable.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 
 import { Disable } from 'react-disable';
-import { UncontrolledTooltip, UncontrolledTooltipProps } from 'reactstrap';
+import type { UncontrolledTooltipProps } from 'reactstrap';
+import { UncontrolledTooltip } from 'reactstrap';
 
 type NotAvailableProps = {
   children: JSX.Element

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

@@ -41,7 +41,7 @@ import {
 import {
   useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData, useSWRxCurrentGrantData,
 } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import { useIsUntitledPage, useSelectedGrant } from '~/stores/ui';
 import { useEditingUsers } from '~/stores/use-editing-users';
@@ -190,6 +190,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
       mutatePageTree();
+
+      mutateRecentlyUpdated();
       // sync current grant data after update
       mutateIsGrantNormalized();
 

+ 2 - 1
apps/app/src/client/components/PageEditor/page-path-rename-utils.ts

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useSWRMUTxCurrentPage } from '~/stores/page';
-import { mutatePageTree, mutatePageList } from '~/stores/page-listing';
+import { mutatePageTree, mutatePageList, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useIsUntitledPage } from '~/stores/ui';
 
 
@@ -33,6 +33,7 @@ export const usePagePathRenameHandler = (
 
     const onRenamed = (fromPath: string | undefined, toPath: string) => {
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutatePageList();
       mutateIsUntitledPage(false);
 

+ 23 - 7
apps/app/src/client/components/SavePageControls.tsx

@@ -2,6 +2,7 @@ import React, { useCallback, useState, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
+import { PageGrant } from '@growi/core';
 import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
@@ -17,9 +18,10 @@ import {
 import { useEditorMode } from '~/stores-universal/ui';
 import { useWaitingSaveProcessing, useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
-import { useIsDeviceLargerThanMd } from '~/stores/ui';
+import { useIsDeviceLargerThanMd, useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
+import { NotAvailable } from './NotAvailable';
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { SlackNotification } from './SlackNotification';
 
@@ -38,6 +40,7 @@ const SavePageButton = (props: {slackChannels: string, isSlackEnabled?: boolean,
   const { t } = useTranslation();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const [isSavePageModalShown, setIsSavePageModalShown] = useState<boolean>(false);
+  const { data: selectedGrant } = useSelectedGrant();
 
   const { slackChannels, isSlackEnabled, isDeviceLargerThanMd } = props;
 
@@ -63,6 +66,7 @@ const SavePageButton = (props: {slackChannels: string, isSlackEnabled?: boolean,
   const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
   const labelUnpublishPage = t('wip_page.save_as_wip');
+  const restrictedGrantOverrideErrorTitle = t('Not available when "anyone with the link" is selected');
 
   return (
     <>
@@ -85,9 +89,15 @@ const SavePageButton = (props: {slackChannels: string, isSlackEnabled?: boolean,
             <>
               <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />
               <DropdownMenu container="body" end>
-                <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
-                  {labelOverwriteScopes}
-                </DropdownItem>
+                <NotAvailable
+                  isDisabled={selectedGrant?.grant === PageGrant.GRANT_RESTRICTED}
+                  classNamePrefix="grw-not-available-when-grant-restricted-is-selected"
+                  title={restrictedGrantOverrideErrorTitle}
+                >
+                  <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
+                    {labelOverwriteScopes}
+                  </DropdownItem>
+                </NotAvailable>
                 <DropdownItem onClick={saveAndMakeWip}>
                   {labelUnpublishPage}
                 </DropdownItem>
@@ -102,9 +112,15 @@ const SavePageButton = (props: {slackChannels: string, isSlackEnabled?: boolean,
                 toggle={() => setIsSavePageModalShown(false)}
               >
                 <div className="d-flex flex-column pt-4 pb-3 px-4 gap-4">
-                  <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndOverwriteScopesOfDescendants() }}>
-                    {labelOverwriteScopes}
-                  </button>
+                  <NotAvailable
+                    isDisabled={selectedGrant?.grant === PageGrant.GRANT_RESTRICTED}
+                    classNamePrefix="grw-not-available-when-grant-restricted-is-selected"
+                    title={restrictedGrantOverrideErrorTitle}
+                  >
+                    <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndOverwriteScopesOfDescendants() }}>
+                      {labelOverwriteScopes}
+                    </button>
+                  </NotAvailable>
                   <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndMakeWip() }}>
                     {labelUnpublishPage}
                   </button>

+ 2 - 1
apps/app/src/client/components/SearchPage/SearchPageBase.tsx

@@ -15,7 +15,7 @@ import {
   useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useIsSearchServiceReachable,
 } from '~/stores-universal/context';
 import { usePageDeleteModal } from '~/stores/modal';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
@@ -275,6 +275,7 @@ export const usePageDeleteModalForBulkDeletion = (
           toastSuccess(t('deleted_pages_completely', { path }));
         }
         mutatePageTree();
+        mutateRecentlyUpdated();
 
         if (onDeleted != null) {
           onDeleted(...args);

+ 4 - 1
apps/app/src/client/components/SearchPage/SearchResultContent.tsx

@@ -21,7 +21,7 @@ import { useCurrentUser } from '~/stores-universal/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
-import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
+import { mutatePageList, mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 
@@ -135,6 +135,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
 
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutateSearching();
       mutatePageList();
     };
@@ -146,6 +147,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('renamed_pages', { path }));
 
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutateSearching();
       mutatePageList();
     };
@@ -165,6 +167,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('deleted_pages', { path }));
     }
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
     mutatePageList();
   }, [t]);

+ 4 - 1
apps/app/src/client/components/SearchPage/SearchResultList.tsx

@@ -12,7 +12,7 @@ import type { ISelectable, ISelectableAll } from '~/client/interfaces/selectable
 import { toastSuccess } from '~/client/util/toastr';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
-import { mutatePageTree, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxPageInfoForList, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
@@ -94,6 +94,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     toastSuccess(t('duplicated_pages', { fromPath }));
 
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
   }, [t]);
 
@@ -101,6 +102,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     toastSuccess(t('renamed_pages', { path }));
 
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
   }, [t]);
 
@@ -118,6 +120,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
       toastSuccess(t('deleted_pages', { path }));
     }
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
   }, [t]);
 

+ 2 - 1
apps/app/src/client/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -8,7 +8,7 @@ import { debounce } from 'throttle-debounce';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import {
-  mutatePageTree, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
+  mutatePageTree, mutateRecentlyUpdated, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
 } from '~/stores/page-listing';
 import { useSidebarScrollerRef } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
@@ -35,6 +35,7 @@ export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: He
   const mutate = useCallback(() => {
     mutateRootPage();
     mutatePageTree();
+    mutateRecentlyUpdated();
   }, [mutateRootPage]);
 
   return (

+ 4 - 6
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -151,13 +151,12 @@ type HeaderProps = {
   onWipPageShownChange: () => void,
 }
 
-const PER_PAGE = 20;
 export const RecentChangesHeader = ({
   isSmall, onSizeChange, isWipPageShown, onWipPageShownChange,
 }: HeaderProps): JSX.Element => {
   const { t } = useTranslation();
 
-  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
+  const { mutate } = useSWRINFxRecentlyUpdated(isWipPageShown, { suspense: true });
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
@@ -232,14 +231,13 @@ type ContentProps = {
 }
 
 export const RecentChangesContent = ({ isSmall, isWipPageShown }: ContentProps): JSX.Element => {
-  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(isWipPageShown, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
 
   const { pushState } = useKeywordManager();
-
   const isEmpty = data?.[0]?.pages.length === 0;
-  const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
-
+  const lastPageIndex = data?.length ? data.length - 1 : 0;
+  const isReachingEnd = isEmpty || (data != null && lastPageIndex > 0 && data[lastPageIndex]?.pages.length < data[lastPageIndex - 1]?.pages.length);
   return (
     <div className="grw-recent-changes">
       <ul className="list-group list-group-flush">

+ 3 - 2
apps/app/src/client/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -11,12 +11,12 @@ import { useRect } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
 import { useCreatePage } from '~/client/services/create-page';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import type { InputValidationResult } from '~/client/util/use-input-validator';
 import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
-import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
 import { shouldCreateWipPage } from '../../../../utils/should-create-wip-page';
@@ -123,6 +123,7 @@ export const useNewPageInput = (): UseNewPageInput => {
             skipTransition: true,
             onCreated: () => {
               mutatePageTree();
+              mutateRecentlyUpdated();
 
               if (!hasDescendants) {
                 stateHandlers?.setIsOpen(true);

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

@@ -9,6 +9,7 @@ import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import {
   useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
 } from '~/stores/page';
+import { mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 
@@ -57,6 +58,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
         router.push(`/${pageId}`);
         mutateCurrentPage();
+        mutateRecentlyUpdated();
       }
       catch (err) {
         const toastError = (await import('~/client/util/toastr')).toastError;

+ 3 - 0
apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx

@@ -26,6 +26,9 @@ export const WipPageAlert = (): JSX.Element => {
       const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
       await mutatePageTree();
 
+      const mutateRecentlyUpdated = (await import('~/stores/page-listing')).mutateRecentlyUpdated;
+      await mutateRecentlyUpdated();
+
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('wip_page.success_publish_page'));
     }

+ 1 - 1
apps/app/src/server/middlewares/apiv3-form-validator.ts

@@ -1,5 +1,5 @@
 import { ErrorV3 } from '@growi/core/dist/models';
-import { NextFunction, Request, Response } from 'express';
+import type { NextFunction, Request, Response } from 'express';
 
 import loggerFactory from '~/utils/logger';
 

+ 9 - 1
apps/app/src/server/routes/apiv3/forgot-password.js

@@ -43,6 +43,14 @@ module.exports = (crowi) => {
           return (value === req.body.newPassword);
         }),
     ],
+    email: [
+      body('email')
+        .isEmail()
+        .escape()
+        .withMessage('message.Email format is invalid')
+        .notEmpty()
+        .withMessage('message.Email field is required'),
+    ],
   };
 
   const checkPassportStrategyMiddleware = checkForgotPasswordEnabledMiddlewareFactory(crowi, true);
@@ -61,7 +69,7 @@ module.exports = (crowi) => {
     });
   }
 
-  router.post('/', checkPassportStrategyMiddleware, addActivity, async(req, res) => {
+  router.post('/', checkPassportStrategyMiddleware, validator.email, apiV3FormValidator, addActivity, async(req, res) => {
     const { email } = req.body;
     const locale = configManager.getConfig('crowi', 'app:globalLang');
     const appUrl = appService.getSiteUrl();

+ 30 - 1
apps/app/src/server/routes/apiv3/revisions.js

@@ -1,6 +1,7 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import express from 'express';
+import { connection } from 'mongoose';
 
 import { Revision } from '~/server/models/revision';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
@@ -14,6 +15,8 @@ const { query, param } = require('express-validator');
 
 const router = express.Router();
 
+const MIGRATION_FILE_NAME = '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549';
+
 /**
  * @swagger
  *  tags:
@@ -110,6 +113,23 @@ module.exports = (crowi) => {
    *            description: Return revisions belong to page
    *
    */
+  let cachedAppliedAt = null;
+
+  const getAppliedAtOfTheMigrationFile = async() => {
+
+    if (cachedAppliedAt != null) {
+      return cachedAppliedAt;
+    }
+
+    const migrationCollection = connection.collection('migrations');
+    const migration = await migrationCollection.findOne({ fileName: { $regex: `^${MIGRATION_FILE_NAME}` } });
+    const appliedAt = migration.appliedAt;
+
+    cachedAppliedAt = appliedAt;
+
+    return appliedAt;
+  };
+
   router.get('/list', certifySharedPage, accessTokenParser, loginRequired, validator.retrieveRevisions, apiV3FormValidator, async(req, res) => {
     const pageId = req.query.pageId;
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
@@ -131,6 +151,9 @@ module.exports = (crowi) => {
 
     try {
       const page = await Page.findOne({ _id: pageId });
+
+      const appliedAt = await getAppliedAtOfTheMigrationFile();
+
       const queryOpts = {
         offset,
         sort: { createdAt: -1 },
@@ -143,8 +166,14 @@ module.exports = (crowi) => {
         queryOpts.pagination = true;
       }
 
+      const queryCondition = {
+        pageId: page._id,
+        createdAt: { $gt: appliedAt },
+      };
+
+      // https://redmine.weseek.co.jp/issues/151652
       const paginateResult = await Revision.paginate(
-        { pageId: page._id },
+        queryCondition,
         queryOpts,
       );
 

+ 2 - 2
apps/app/src/server/routes/forgot-password.ts

@@ -1,4 +1,4 @@
-import {
+import type {
   NextFunction, Request, Response,
 } from 'express';
 import createError from 'http-errors';
@@ -6,7 +6,7 @@ import createError from 'http-errors';
 import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
 import loggerFactory from '~/utils/logger';
 
-import { IPasswordResetOrder } from '../models/password-reset-order';
+import type { IPasswordResetOrder } from '../models/password-reset-order';
 
 const logger = loggerFactory('growi:routes:forgot-password');
 

+ 1 - 1
apps/app/src/server/service/page/index.ts

@@ -4109,7 +4109,7 @@ class PageService implements IPageService {
     }
 
     // 3. Update scopes for descendants
-    if (options.overwriteScopesOfDescendants) {
+    if (options.overwriteScopesOfDescendants && shouldBeOnTree) {
       await this.applyScopesToDescendantsWithStream(currentPage, user);
     }
 

+ 34 - 13
apps/app/src/stores/page-listing.tsx

@@ -7,9 +7,10 @@ import type {
 import useSWR, {
   mutate, type SWRConfiguration, type SWRResponse, type Arguments,
 } from 'swr';
+import { cache } from 'swr/_internal';
 import useSWRImmutable from 'swr/immutable';
 import type { SWRInfiniteResponse } from 'swr/infinite';
-import useSWRInfinite from 'swr/infinite';
+import useSWRInfinite, { unstable_serialize } from 'swr/infinite'; // eslint-disable-line camelcase
 
 import type { IPagingResult } from '~/interfaces/paging-result';
 
@@ -33,27 +34,47 @@ type RecentApiResult = {
   totalCount: number,
   offset: number,
 }
-export const useSWRINFxRecentlyUpdated = (limit: number, includeWipPage?: boolean, config?: SWRConfiguration) : SWRInfiniteResponse<RecentApiResult, Error> => {
-  return useSWRInfinite(
-    (pageIndex, previousPageData) => {
-      if (previousPageData != null && previousPageData.pages.length === 0) return null;
 
-      if (pageIndex === 0 || previousPageData == null) {
-        return ['/pages/recent', undefined, limit, includeWipPage];
-      }
+export const getRecentlyUpdatedKey = (
+    pageIndex: number,
+    previousPageData: RecentApiResult | null,
+    includeWipPage?: boolean,
+): [string, number | undefined, boolean | undefined] | null => {
+  if (previousPageData != null && previousPageData.pages.length === 0) return null;
 
-      const offset = previousPageData.offset + limit;
-      return ['/pages/recent', offset, limit, includeWipPage];
-    },
-    ([endpoint, offset, limit, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit, includeWipPage }).then(response => response.data),
+  if (pageIndex === 0 || previousPageData == null) {
+    return ['/pages/recent', undefined, includeWipPage];
+  }
+  const offset = previousPageData.offset + previousPageData.pages.length;
+  return ['/pages/recent', offset, includeWipPage];
+
+};
+
+export const useSWRINFxRecentlyUpdated = (
+    includeWipPage?: boolean,
+    config?: SWRConfiguration,
+): SWRInfiniteResponse<RecentApiResult, Error> => {
+  const PER_PAGE = 20;
+  return useSWRInfinite(
+    (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+    ([endpoint, offset, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit: PER_PAGE, includeWipPage }).then(response => response.data),
     {
       ...config,
       revalidateFirstPage: false,
-      revalidateAll: false,
+      revalidateAll: true,
     },
   );
 };
 
+export const mutateRecentlyUpdated = async(): Promise<undefined> => {
+  [true, false].forEach(includeWipPage => mutate(
+    unstable_serialize(
+      (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+    ),
+  ));
+  return;
+};
+
 export const mutatePageList = async(): Promise<void[]> => {
   return mutate(
     key => Array.isArray(key) && key[0] === '/pages/list',

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.0.20-slackbot-proxy.0",
+  "version": "7.0.21-slackbot-proxy.0",
   "license": "MIT",
   "private": "true",
   "scripts": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.0.20",
+  "version": "7.0.21-RC.0",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "private": "true",

+ 12 - 12
packages/editor/package.json

@@ -28,26 +28,26 @@
     "@codemirror/merge": "Fixed version at 6.0.0 due to errors caused by dependent packages"
   },
   "devDependencies": {
-    "@codemirror/lang-markdown": "^6.2.0",
-    "@codemirror/language": "^6.8.0",
-    "@codemirror/language-data": "^6.3.1",
+    "@codemirror/lang-markdown": "^6.3.0",
+    "@codemirror/language": "^6.10.3",
+    "@codemirror/language-data": "^6.5.1",
     "@codemirror/merge": "6.0.0",
-    "@codemirror/state": "^6.2.1",
-    "@codemirror/view": "^6.15.3",
+    "@codemirror/state": "^6.4.1",
+    "@codemirror/view": "^6.34.1",
     "@emoji-mart/data": "^1.2.1",
     "@emoji-mart/react": "^1.1.1",
     "@growi/core": "link:../core",
     "@growi/core-styles": "link:../core-styles",
     "@popperjs/core": "^2.11.8",
-    "@replit/codemirror-emacs": "^6.0.1",
-    "@replit/codemirror-vim": "6.0.14",
+    "@replit/codemirror-emacs": "^6.1.0",
+    "@replit/codemirror-vim": "6.2.1",
     "@replit/codemirror-vscode-keymap": "^6.0.2",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",
-    "@uiw/codemirror-theme-eclipse": "^4.21.21",
-    "@uiw/codemirror-theme-kimbie": "^4.21.21",
-    "@uiw/codemirror-themes": "^4.21.21",
-    "@uiw/react-codemirror": "^4.21.8",
+    "@uiw/codemirror-theme-eclipse": "^4.23.5",
+    "@uiw/codemirror-theme-kimbie": "^4.23.5",
+    "@uiw/codemirror-themes": "^4.23.5",
+    "@uiw/react-codemirror": "^4.23.5",
     "bootstrap": "=5.3.2",
     "cm6-theme-basic-light": "^0.2.0",
     "cm6-theme-material-dark": "^0.2.0",
@@ -66,6 +66,6 @@
     "ts-deepmerge": "^6.2.0",
     "y-codemirror.next": "^0.3.5",
     "y-socket.io": "^1.1.3",
-    "yjs": "^13.6.18"
+    "yjs": "^13.6.19"
   }
 }

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

@@ -52,7 +52,7 @@
     "express": "^4.20.0",
     "hast-util-select": "^5.0.5",
     "mongoose": "^6.11.3",
-    "swr": "^2.0.3",
+    "swr": "^2.2.2",
     "universal-bunyan": "^0.9.2",
     "xss": "^1.0.15"
   },

+ 3 - 1
packages/remark-lsx/package.json

@@ -38,9 +38,11 @@
     "@growi/ui": "link:../ui",
     "escape-string-regexp": "^4.0.0",
     "express": "^4.20.0",
+    "express-validator": "^6.14.0",
     "http-errors": "^2.0.0",
     "mongoose": "^6.11.3",
-    "swr": "^2.2.2"
+    "swr": "^2.2.2",
+    "xss": "^1.0.15"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 39 - 2
packages/remark-lsx/src/server/index.ts

@@ -1,4 +1,8 @@
-import type { Request, Response } from 'express';
+import type { NextFunction, Request, Response } from 'express';
+import { query, validationResult } from 'express-validator';
+import { FilterXSS } from 'xss';
+
+import type { LsxApiOptions } from '../interfaces/api';
 
 import { listPages } from './routes/list-pages';
 
@@ -6,12 +10,45 @@ const loginRequiredFallback = (req: Request, res: Response) => {
   return res.status(403).send('login required');
 };
 
+const filterXSS = new FilterXSS();
+
+const lsxValidator = [
+  query('pagePath').notEmpty().isString(),
+  query('offset').optional().isInt(),
+  query('limit').optional().isInt(),
+  query('options')
+    .optional()
+    .customSanitizer((options) => {
+      try {
+        const jsonData: LsxApiOptions = JSON.parse(options);
+
+        Object.keys(jsonData).forEach((key) => {
+          jsonData[key] = filterXSS.process(jsonData[key]);
+        });
+
+        return jsonData;
+      }
+      catch (err) {
+        throw new Error('Invalid JSON format in options');
+      }
+    }),
+  query('options.*').optional().isString(),
+];
+
+const paramValidator = (req: Request, _: Response, next: NextFunction) => {
+  const errObjArray = validationResult(req);
+  if (errObjArray.isEmpty()) {
+    return next();
+  }
+  return new Error('Invalid lsx parameter');
+};
+
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
 const middleware = (crowi: any, app: any): void => {
   const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
   const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
 
-  app.get('/_api/lsx', accessTokenParser, loginRequired, listPages);
+  app.get('/_api/lsx', accessTokenParser, loginRequired, lsxValidator, paramValidator, listPages);
 };
 
 export default middleware;

+ 8 - 5
packages/remark-lsx/src/server/routes/list-pages/index.spec.ts

@@ -3,12 +3,15 @@ import type { Request, Response } from 'express';
 import createError from 'http-errors';
 import { mock } from 'vitest-mock-extended';
 
-import type { LsxApiResponseData } from '../../../interfaces/api';
+import type { LsxApiResponseData, LsxApiParams } from '../../../interfaces/api';
 
 import type { PageQuery, PageQueryBuilder } from './generate-base-query';
 
 import { listPages } from '.';
 
+interface IListPagesRequest extends Request<undefined, undefined, undefined, LsxApiParams> {
+  user: IUser,
+}
 
 // mocking modules
 const mocks = vi.hoisted(() => {
@@ -30,7 +33,7 @@ describe('listPages', () => {
 
   it("returns 400 HTTP response when the query 'pagePath' is undefined", async() => {
     // setup
-    const reqMock = mock<Request & { user: IUser }>();
+    const reqMock = mock<IListPagesRequest>();
     const resMock = mock<Response>();
     const resStatusMock = mock<Response>();
     resMock.status.calledWith(400).mockReturnValue(resStatusMock);
@@ -46,7 +49,7 @@ describe('listPages', () => {
 
   describe('with num option', () => {
 
-    const reqMock = mock<Request & { user: IUser }>();
+    const reqMock = mock<IListPagesRequest>();
     reqMock.query = { pagePath: '/Sandbox' };
 
     const builderMock = mock<PageQueryBuilder>();
@@ -97,7 +100,7 @@ describe('listPages', () => {
 
     it('returns 500 HTTP response when an unexpected error occured', async() => {
       // setup
-      const reqMock = mock<Request & { user: IUser }>();
+      const reqMock = mock<IListPagesRequest>();
       reqMock.query = { pagePath: '/Sandbox' };
 
       // an Error instance will be thrown by addNumConditionMock
@@ -124,7 +127,7 @@ describe('listPages', () => {
 
     it('returns 400 HTTP response when the value is invalid', async() => {
       // setup
-      const reqMock = mock<Request & { user: IUser }>();
+      const reqMock = mock<IListPagesRequest>();
       reqMock.query = { pagePath: '/Sandbox' };
 
       // an http-errors instance will be thrown by addNumConditionMock

+ 10 - 7
packages/remark-lsx/src/server/routes/list-pages/index.ts

@@ -56,20 +56,23 @@ function addExceptCondition(query, pagePath, optionsFilter): PageQuery {
   return addFilterCondition(query, pagePath, optionsFilter, true);
 }
 
+interface IListPagesRequest extends Request<undefined, undefined, undefined, LsxApiParams> {
+  user: IUser,
+}
+
 
-export const listPages = async(req: Request & { user: IUser }, res: Response): Promise<Response> => {
+export const listPages = async(req: IListPagesRequest, res: Response): Promise<Response> => {
   const user = req.user;
 
-  // TODO: use express-validator
   if (req.query.pagePath == null) {
-    return res.status(400).send("The 'pagePath' query must not be null.");
+    return res.status(400).send("the 'pagepath' query must not be null.");
   }
 
   const params: LsxApiParams = {
-    pagePath: removeTrailingSlash(req.query.pagePath.toString()),
-    offset: req.query?.offset != null ? Number(req.query.offset) : undefined,
-    limit: req.query?.limit != null ? Number(req.query?.limit) : undefined,
-    options: req.query?.options != null ? JSON.parse(req.query.options.toString()) : {},
+    pagePath: removeTrailingSlash(req.query.pagePath),
+    offset: req.query?.offset,
+    limit: req.query?.limit,
+    options: req.query?.options ?? {},
   };
 
   const {

+ 54 - 84
yarn.lock

@@ -1614,10 +1614,10 @@
     "@lezer/highlight" "^1.0.0"
     "@lezer/lr" "^1.3.1"
 
-"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.2.0":
-  version "6.2.5"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.2.5.tgz#451941bf743d3788e73598f1aedb71cbeb6f71ba"
-  integrity sha512-Hgke565YcO4fd9pe2uLYxnMufHO5rQwRr+AAhFq8ABuhkrjyX8R5p5s+hZUTdV60O0dMRjxKhBLxz8pu/MkUVA==
+"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.3.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.3.0.tgz#949f8803332441705ed6def34c565f2166479538"
+  integrity sha512-lYrI8SdL/vhd0w0aHIEvIRLRecLF7MiiRfzXFZY94dFwHqC9HtgxgagJ8fyYNBldijGatf9wkms60d8SrAj6Nw==
   dependencies:
     "@codemirror/autocomplete" "^6.7.1"
     "@codemirror/lang-html" "^6.0.0"
@@ -1726,7 +1726,7 @@
     "@lezer/highlight" "^1.2.0"
     "@lezer/yaml" "^1.0.0"
 
-"@codemirror/language-data@^6.3.1":
+"@codemirror/language-data@^6.5.1":
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/@codemirror/language-data/-/language-data-6.5.1.tgz#5cb9413d5225ef27a577c23781bbc0b36c58bb67"
   integrity sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==
@@ -1754,10 +1754,10 @@
     "@codemirror/language" "^6.0.0"
     "@codemirror/legacy-modes" "^6.4.0"
 
-"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
-  version "6.10.2"
-  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
-  integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.3", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
+  version "6.10.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.3.tgz#eb25fc5ade19032e7bf1dcaa957804e5f1660585"
+  integrity sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==
   dependencies:
     "@codemirror/state" "^6.0.0"
     "@codemirror/view" "^6.23.0"
@@ -1799,7 +1799,7 @@
     "@codemirror/view" "^6.0.0"
     crelt "^1.0.5"
 
-"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.2.1", "@codemirror/state@^6.4.0":
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1":
   version "6.4.1"
   resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
   integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
@@ -1814,10 +1814,10 @@
     "@codemirror/view" "^6.0.0"
     "@lezer/highlight" "^1.0.0"
 
-"@codemirror/view@^6.0.0", "@codemirror/view@^6.15.3", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0":
-  version "6.33.0"
-  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.33.0.tgz#51e270410fc3af92a6e38798e80ebf8add7dc3ec"
-  integrity sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.34.1":
+  version "6.34.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.34.1.tgz#b17ed29c563e4adc60086233f2d3e7197e2dc33e"
+  integrity sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==
   dependencies:
     "@codemirror/state" "^6.4.0"
     style-mod "^4.1.0"
@@ -2207,7 +2207,7 @@
     express "^4.20.0"
     hast-util-select "^5.0.5"
     mongoose "^6.11.3"
-    swr "^2.0.3"
+    swr "^2.2.2"
     universal-bunyan "^0.9.2"
     xss "^1.0.15"
 
@@ -3220,15 +3220,15 @@
   resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
   integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
 
-"@replit/codemirror-emacs@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@replit/codemirror-emacs/-/codemirror-emacs-6.0.1.tgz#6e74453e456f40cbb18ed1d15030fa0dbd218098"
-  integrity sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==
+"@replit/codemirror-emacs@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz#662dffc3b354c47cbf930219f8cb75cfc9e7f6fe"
+  integrity sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==
 
-"@replit/codemirror-vim@6.0.14":
-  version "6.0.14"
-  resolved "https://registry.yarnpkg.com/@replit/codemirror-vim/-/codemirror-vim-6.0.14.tgz#8f44740b0497406b551726946c9b30f21c867671"
-  integrity sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==
+"@replit/codemirror-vim@6.2.1":
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/@replit/codemirror-vim/-/codemirror-vim-6.2.1.tgz#6673ff4be93b7da03d303ef37d6cbfa5f647b74b"
+  integrity sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==
 
 "@replit/codemirror-vscode-keymap@^6.0.2":
   version "6.0.2"
@@ -4992,10 +4992,10 @@
     "@typescript-eslint/types" "6.21.0"
     eslint-visitor-keys "^3.4.1"
 
-"@uiw/codemirror-extensions-basic-setup@4.23.0":
-  version "4.23.0"
-  resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.0.tgz#c3c181153335c208a25d59b8ecbc7fc87fe85356"
-  integrity sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==
+"@uiw/codemirror-extensions-basic-setup@4.23.5":
+  version "4.23.5"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.5.tgz#02ebe9c44f76609f15295e1ff9c83e770140c369"
+  integrity sha512-eTMfT8TejVN/D5vvuz9Lab+MIoRYdtqa2ftZZmU3JpcDIXf9KaExPo+G2Rl9HqySzaasgGXOOG164MAnj3MSIw==
   dependencies:
     "@codemirror/autocomplete" "^6.0.0"
     "@codemirror/commands" "^6.0.0"
@@ -5005,39 +5005,39 @@
     "@codemirror/state" "^6.0.0"
     "@codemirror/view" "^6.0.0"
 
-"@uiw/codemirror-theme-eclipse@^4.21.21":
-  version "4.23.0"
-  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-eclipse/-/codemirror-theme-eclipse-4.23.0.tgz#22c66297e6b5ca944528c84a46943d846694a2aa"
-  integrity sha512-P48jiLcaIdNJKWMgG/oH9pP6a8w5lW8lM7VWoCAfSs+v0Sj7ErJGtODGQFcOX0pzI7bc64Se5oNMWPMrtQf3Zw==
+"@uiw/codemirror-theme-eclipse@^4.23.5":
+  version "4.23.5"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-eclipse/-/codemirror-theme-eclipse-4.23.5.tgz#6b9f8d66fa63701395322353011bc0a5ecb3998d"
+  integrity sha512-WsfcdDjQQCpd42KUJcp/2r5UjTvXetR5V6Zp12ZJliL1gpjtHdZ6ZJxSwQhga6pfEBn2ITz5u5lZgBv/zXgtqg==
   dependencies:
-    "@uiw/codemirror-themes" "4.23.0"
+    "@uiw/codemirror-themes" "4.23.5"
 
-"@uiw/codemirror-theme-kimbie@^4.21.21":
-  version "4.23.0"
-  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-kimbie/-/codemirror-theme-kimbie-4.23.0.tgz#df24bb54947b9d058a1ba146206c644ff982400a"
-  integrity sha512-hARic9WzsVSrT3uiuZeYSxyLmNU2LgoCD7hWauOJZm7HQ2EylVKV+xmcg/WkWMOCCyOZYRxn/pd0NWjRQmMVsA==
+"@uiw/codemirror-theme-kimbie@^4.23.5":
+  version "4.23.5"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-kimbie/-/codemirror-theme-kimbie-4.23.5.tgz#ab3e6f179b1fa2b724b1862178119ca7cc89cde2"
+  integrity sha512-vC3kr0Lr5AhN2J4KhMYHObRovl3aTBrDMC44JSLzgwU+EBceNPEMrFKfY0pnDgyCXof95R8k4+cfH0WjfQFvtA==
   dependencies:
-    "@uiw/codemirror-themes" "4.23.0"
+    "@uiw/codemirror-themes" "4.23.5"
 
-"@uiw/codemirror-themes@4.23.0", "@uiw/codemirror-themes@^4.21.21":
-  version "4.23.0"
-  resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.0.tgz#cc5b5242d3e67caf49c2a9120e804b16ad79f86d"
-  integrity sha512-9fiji9xooZyBQozR1i6iTr56YP7j/Dr/VgsNWbqf5Szv+g+4WM1iZuiDGwNXmFMWX8gbkDzp6ASE21VCPSofWw==
+"@uiw/codemirror-themes@4.23.5", "@uiw/codemirror-themes@^4.23.5":
+  version "4.23.5"
+  resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.5.tgz#742cb8f2a74a857cb44c5f588265865ee2327e91"
+  integrity sha512-yWUTpaVroxIxjKASQAmKaYy+ZYtF+YB6d8sVmSRK2TVD13M+EWvVT2jBGFLqR1UVg7G0W/McAy8xdeTg+a3slg==
   dependencies:
     "@codemirror/language" "^6.0.0"
     "@codemirror/state" "^6.0.0"
     "@codemirror/view" "^6.0.0"
 
-"@uiw/react-codemirror@^4.21.8":
-  version "4.23.0"
-  resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.0.tgz#5eeadcd5de61213ad76ac3c772fffb7e5b54b465"
-  integrity sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==
+"@uiw/react-codemirror@^4.23.5":
+  version "4.23.5"
+  resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.5.tgz#6a16c23062067732cba105ac33ad69cf8e5c2111"
+  integrity sha512-2zzGpx61L4mq9zDG/hfsO4wAH209TBE8VVsoj/qrccRe6KfcneCwKgRxtQjxBCCnO0Q5S+IP+uwCx5bXRzgQFQ==
   dependencies:
     "@babel/runtime" "^7.18.6"
     "@codemirror/commands" "^6.1.0"
     "@codemirror/state" "^6.1.1"
     "@codemirror/theme-one-dark" "^6.0.0"
-    "@uiw/codemirror-extensions-basic-setup" "4.23.0"
+    "@uiw/codemirror-extensions-basic-setup" "4.23.5"
     codemirror "^6.0.0"
 
 "@vitejs/plugin-react@^4.3.1":
@@ -15093,11 +15093,6 @@ react-card-flip@^1.0.10:
   resolved "https://registry.yarnpkg.com/react-card-flip/-/react-card-flip-1.0.10.tgz#f3eab968f2cba6de6eccb84cf73bcaf6b53fb974"
   integrity sha512-BqK6PmP+L/xmcH1AoMuirbxRuDIiaNy3r8734GJQqEyIWoW8L4j2c/di6mbNg+I2rGue3tLH1I9QbJLd7M89ww==
 
-react-codemirror2@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525"
-  integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==
-
 react-copy-to-clipboard@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
@@ -17001,7 +16996,7 @@ string-template@>=1.0.0:
   resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
   integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
 
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -17019,15 +17014,6 @@ string-width@=4.2.2:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
 string-width@^5.0.1, string-width@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -17111,7 +17097,7 @@ stringify-entities@^4.0.0:
     character-entities-html4 "^2.0.0"
     character-entities-legacy "^3.0.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17125,13 +17111,6 @@ strip-ansi@^3.0.0:
   dependencies:
     ansi-regex "^2.0.0"
 
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
 strip-ansi@^7.0.1, strip-ansi@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -17504,7 +17483,7 @@ swagger2openapi@^7.0.8:
     yaml "^1.10.0"
     yargs "^17.0.1"
 
-swr@^2.0.3, swr@^2.2.2:
+swr@^2.2.2:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
   integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
@@ -18926,7 +18905,7 @@ word-wrap@^1.2.3:
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -18944,15 +18923,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -19281,10 +19251,10 @@ yauzl@^2.10.0:
     buffer-crc32 "~0.2.3"
     fd-slicer "~1.1.0"
 
-yjs@^13.6.18:
-  version "13.6.18"
-  resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.18.tgz#d1575203478bc99ad1b89c098e7d4bacb7f91c3b"
-  integrity sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==
+yjs@^13.6.18, yjs@^13.6.19:
+  version "13.6.19"
+  resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.19.tgz#66999f41254ab65be8c8e71bd767d124ad600909"
+  integrity sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==
   dependencies:
     lib0 "^0.2.86"