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

Merge branch 'master' into feat/openai-vector-searching

Shun Miyazawa 1 год назад
Родитель
Сommit
ae71d9c53b
51 измененных файлов с 529 добавлено и 260 удалено
  1. 20 18
      .github/mergify.yml
  2. 3 2
      .github/workflows/auto-labeling.yml
  3. 1 0
      .github/workflows/ci-app-prod.yml
  4. 3 2
      .github/workflows/ci-app.yml
  5. 4 0
      .github/workflows/ci-slackbot-proxy.yml
  6. 1 1
      .github/workflows/release-slackbot-proxy.yml
  7. 34 1
      CHANGELOG.md
  8. 1 1
      apps/app/docker/README.md
  9. 5 5
      apps/app/package.json
  10. 6 0
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  11. 3 3
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  12. 1 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  13. 1 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  14. 2 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  15. 28 25
      apps/app/src/client/components/Bookmarks/BookmarkFolderItemControl.tsx
  16. 12 9
      apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.tsx
  17. 47 30
      apps/app/src/client/components/Common/Dropdown/PageItemControl.spec.tsx
  18. 14 12
      apps/app/src/client/components/Common/Dropdown/PageItemControl.tsx
  19. 13 10
      apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx
  20. 6 2
      apps/app/src/client/components/PageComment.tsx
  21. 1 1
      apps/app/src/client/components/PageComment/ReplyComments.tsx
  22. 1 0
      apps/app/src/interfaces/apiv3/page.ts
  23. 0 14
      apps/app/src/models/admin/growi-archive-import-option.js
  24. 18 0
      apps/app/src/models/admin/growi-archive-import-option.ts
  25. 0 0
      apps/app/src/models/admin/import-mode.ts
  26. 5 3
      apps/app/src/models/admin/import-option-for-pages.ts
  27. 0 13
      apps/app/src/models/admin/import-option-for-revisions.js
  28. 15 0
      apps/app/src/models/admin/import-option-for-revisions.ts
  29. 8 4
      apps/app/src/pages/[[...path]].page.tsx
  30. 1 0
      apps/app/src/server/models/page.ts
  31. 1 1
      apps/app/src/server/routes/apiv3/import.js
  32. 15 5
      apps/app/src/server/routes/apiv3/page/index.ts
  33. 10 2
      apps/app/src/server/routes/apiv3/page/update-page.ts
  34. 4 3
      apps/app/src/server/routes/comment.js
  35. 5 4
      apps/app/src/server/service/g2g-transfer.ts
  36. 2 1
      apps/app/src/server/service/import/import-settings.ts
  37. 5 7
      apps/app/src/server/service/import/import.ts
  38. 0 2
      apps/app/src/server/service/import/index.ts
  39. 1 1
      apps/app/src/server/service/import/overwrite-params/index.ts
  40. 14 2
      apps/app/src/stores/page.tsx
  41. 3 2
      apps/app/turbo.json
  42. 1 1
      apps/slackbot-proxy/package.json
  43. 3 1
      apps/slackbot-proxy/src/services/LinkSharedService.ts
  44. 42 0
      apps/slackbot-proxy/turbo.json
  45. 1 1
      package.json
  46. 1 1
      packages/remark-attachment-refs/package.json
  47. 1 1
      packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts
  48. 1 1
      packages/remark-lsx/package.json
  49. 2 2
      packages/slack/src/interfaces/growi-event-processor.ts
  50. 0 21
      turbo.json
  51. 163 42
      yarn.lock

+ 20 - 18
.github/mergify.yml

@@ -2,34 +2,36 @@ queue_rules:
   - name: default
     allow_inplace_checks: false
     queue_conditions:
-      - '#approved-reviews-by >= 2'
-      - check-success = "check-title"
-      - check-success = "Node CI for slackbot-proxy"
-      - check-success = "Node CI for app development"
+      - check-success ~= ci-app-lint
+      - check-success ~= ci-app-test
+      - check-success ~= ci-app-launch-dev
+      - -check-failure ~= ci-app-
+      - -check-failure ~= ci-slackbot-
+      - -check-failure ~= test-prod-node20 /
     merge_conditions:
-      - '#approved-reviews-by >= 2'
-      - check-success = "check-title"
-      - check-success = "Node CI for slackbot-proxy"
-      - check-success = "Node CI for app development"
-      - check-success = "Node CI for app production"
+      - check-success ~= ci-app-lint
+      - check-success ~= ci-app-test
+      - check-success ~= ci-app-launch-dev
+      - check-success = test-prod-node20 / build-prod
+      - check-success = test-prod-node20 / launch-prod
+      - check-success ~= test-prod-node20 / run-playwright
+      - -check-failure ~= ci-app-
+      - -check-failure ~= ci-slackbot-
+      - -check-failure ~= test-prod-node20 /
 
 pull_request_rules:
-  - name: Automatic merge for Dependabot pull requests
+  - name: Automatic queue to merge
     conditions:
-      - author = dependabot[bot]
       - '#approved-reviews-by >= 1'
-      - check-success = "check-title"
-      - check-success ~= Node CI for slackbot-proxy / .*
-      - check-success ~= Node CI for app development / .*
-      - check-success ~= Node CI for app production / .*
+      - '#review-requested = 0'
+      - check-success = check-title
     actions:
-      merge:
-        method: merge
+      queue:
 
   - name: Automatic merge for Preparing next version
     conditions:
       - author = github-actions[bot]
-      - label = "type/prepare-next-version"
+      - label = type/prepare-next-version
     actions:
       merge:
         method: merge

+ 3 - 2
.github/workflows/auto-labeling.yml

@@ -21,7 +21,8 @@ jobs:
 
     if: |
       (!contains( github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog' )
-        && !startsWith( github.head_ref, 'changeset-release/' ))
+        && !startsWith( github.head_ref, 'changeset-release/' )
+        && !startsWith( github.head_ref, 'mergify/merge-queue/' ))
 
     steps:
       - uses: release-drafter/release-drafter@v5
@@ -36,7 +37,7 @@ jobs:
     if: |
       (!contains( github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog' )
         && !startsWith( github.head_ref, 'changeset-release/' )
-        && !startsWith( github.head_ref, 'dependabot/' ))
+        && !startsWith( github.head_ref, 'mergify/merge-queue/' ))
 
     steps:
       - uses: amannn/action-semantic-pull-request@v5

+ 1 - 0
.github/workflows/ci-app-prod.yml

@@ -23,6 +23,7 @@ on:
       - master
       - dev/7.*.x
       - dev/6.*.x
+      - release/*
     types: [opened, reopened, synchronize]
     paths:
       - .github/mergify.yml

+ 3 - 2
.github/workflows/ci-app.yml

@@ -7,6 +7,7 @@ on:
       - rc/**
       - changeset-release/**
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app.yml
       - .eslint*
       - tsconfig.base.json
@@ -205,11 +206,11 @@ jobs:
           yarn global add node-gyp
           yarn --frozen-lockfile
 
-      - name: turbo run dev:ci
+      - name: turbo run launch-dev:ci
         working-directory: ./apps/app
         run: |
           cp config/ci/.env.local.for-ci .env.development.local
-          turbo run dev:ci --env-mode=loose
+          turbo run launch-dev:ci --env-mode=loose
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
 

+ 4 - 0
.github/workflows/ci-slackbot-proxy.yml

@@ -7,6 +7,7 @@ on:
       - rc/**
       - support/prepare-v**
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-slackbot-proxy.yml
       - .eslint*
       - tsconfig.base.json
@@ -175,6 +176,9 @@ jobs:
 
 
   ci-slackbot-proxy-launch-prod:
+
+    if: startsWith(github.head_ref, 'mergify/merge-queue/')
+
     runs-on: ubuntu-latest
 
     strategy:

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -48,7 +48,7 @@ jobs:
         gcloud auth configure-docker --quiet
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v2
+      uses: docker/setup-buildx-action@v3
 
     - name: Build and push
       uses: docker/build-push-action@v4

+ 34 - 1
CHANGELOG.md

@@ -1,9 +1,42 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.17...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.19...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.0.19](https://github.com/weseek/growi/compare/v7.0.18...v7.0.19) - 2024-09-12
+
+### 🐛 Bug Fixes
+
+* fix: Shared page is not displayed when skipping SSR (#9089) @miya
+* fix: The grant of pages can be changed via api even if restricted (#9087) @WNomunomu
+* fix: Updated content is not reflected on the View screen even after refreshing the page (#9086) @miya
+* fix: Removing comment doesn't work (#9083) @yuki-takei
+
+## [v7.0.18](https://github.com/weseek/growi/compare/v7.0.17...v7.0.18) - 2024-09-09
+
+### 🚀 Improvement
+
+* imprv: Prevent looping to update a hook for TrashPageAlert (#9066) @yuki-takei
+* imprv: Display page tree in page select modal with scrollbar (#9023) @kazutoweseek
+
+### 🐛 Bug Fixes
+
+* fix: issue that material symbols icons are not displayed in ReplyComments component (#9076) @WNomunomu
+* fix: Unable to navigate to the data transfer page (#9071) @miya
+* fix: Page content does not update when switching revisions (#9072) @miya
+* fix: Supress rendering too many invisible DropdownMenu components (#9073) @yuki-takei
+* fix: Return error when grant is string for PUT /_api/v3/page (#9069) @arafubeatbox
+* fix: Scrolling may not occurs when clicking on the edit button next to the header (#9043) @reiji-h
+* fix: API v3 Page update (#9053) @maeshinshin
+* fix: Input text becomes empty when opening the ReadOnlyEditor (#9059) @miya
+* fix: Show pages with grants that are set to be visible in security settings on RecentChanges and PageTree as well (#9044) @miya
+
+### 🧰 Maintenance
+
+* support: Omit Cypress (#9065) @miya
+* ci(deps): bump unzip-stream from 0.3.1 to 0.3.2 (#9049) @dependabot
+
 ## [v7.0.17](https://github.com/weseek/growi/compare/v7.0.16...v7.0.17) - 2024-08-26
 
 ### 🚀 Improvement

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

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

+ 5 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.18-RC.0",
+  "version": "7.0.20-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -26,7 +26,7 @@
     "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "//// for CI": "",
-    "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
+    "launch-dev:ci": "yarn cross-env NODE_ENV=development yarn dev:migrate && yarn ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tspc",
     "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
@@ -91,7 +91,7 @@
     "async-canvas-to-blob": "^1.0.3",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
-    "body-parser": "^1.18.2",
+    "body-parser": "^1.20.3",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
@@ -113,7 +113,7 @@
     "escape-string-regexp": "^4.0.0",
     "eslint-plugin-regex": "^1.8.0",
     "expose-gc": "^1.0.0",
-    "express": "^4.19.2",
+    "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
     "express-mongo-sanitize": "^2.1.0",
     "express-session": "^1.16.1",
@@ -150,7 +150,7 @@
     "next-themes": "^0.2.1",
     "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
-    "nodemailer": "^6.9.14",
+    "nodemailer": "^6.9.15",
     "nodemailer-ses-transport": "~1.5.0",
     "openai": "^4.56.0",
     "openid-client": "^5.4.0",

+ 6 - 0
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -50,6 +50,12 @@ test('admin/export is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
 });
 
+test('admin/data-transfer is successfully loaded', async({ page }) => {
+  await page.goto('/admin/data-transfer');
+
+  await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
+});
+
 test('admin/notification is successfully loaded', async({ page }) => {
   await page.goto('/admin/notification');
 

+ 3 - 3
apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import { useTranslation } from 'next-i18next';
 
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 
@@ -22,7 +22,7 @@ const GROUPS_CONFIG = [
 ];
 const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
-const IMPORT_OPTION_CLASS_MAPPING = {
+const IMPORT_OPTION_CLASS_MAPPING: Record<string, typeof GrowiArchiveImportOption> = {
   pages: ImportOptionForPages,
   revisions: ImportOptionForRevisions,
 };
@@ -188,7 +188,7 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
         ? MODE_RESTRICTED_COLLECTION[collectionName][0]
         : DEFAULT_MODE;
       const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      initialOptionsMap[collectionName] = new ImportOption(initialMode);
+      initialOptionsMap[collectionName] = new ImportOption(collectionName, initialMode);
     });
     updateOptionsMap(initialOptionsMap);
   }, [allCollectionNames, updateOptionsMap]);

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

@@ -11,7 +11,7 @@ import {
   ModalFooter,
 } from 'reactstrap';
 
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 

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

@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Progress } from 'reactstrap';
 
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 
 const MODE_ATTR_MAP = {

+ 2 - 1
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 import { useAdminSocket } from '~/stores/socket-io';
@@ -27,6 +27,7 @@ const GROUPS_CONFIG = [
 ];
 const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
+/** @type Record<string, typeof GrowiArchiveImportOption> */
 const IMPORT_OPTION_CLASS_MAPPING = {
   pages: ImportOptionForPages,
   revisions: ImportOptionForRevisions,

+ 28 - 25
apps/app/src/client/components/Bookmarks/BookmarkFolderItemControl.tsx

@@ -26,37 +26,40 @@ export const BookmarkFolderItemControl: React.FC<{
           <span className="material-symbols-outlined">more_horiz</span>
         </DropdownToggle>
       ) }
-      <DropdownMenu
-        container="body"
-        style={{ zIndex: 1055 }} /* make it larger than $zindex-modal of bootstrap */
-      >
-        {onClickMoveToRoot && (
+
+      { isOpen && (
+        <DropdownMenu
+          container="body"
+          style={{ zIndex: 1055 }}
+        >
+          {onClickMoveToRoot && (
+            <DropdownItem
+              onClick={onClickMoveToRoot}
+              className="grw-page-control-dropdown-item"
+            >
+              <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
+              {t('bookmark_folder.move_to_root')}
+            </DropdownItem>
+          )}
           <DropdownItem
-            onClick={onClickMoveToRoot}
+            onClick={onClickRename}
             className="grw-page-control-dropdown-item"
           >
-            <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
-            {t('bookmark_folder.move_to_root')}
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">redo</span>
+            {t('Rename')}
           </DropdownItem>
-        )}
-        <DropdownItem
-          onClick={onClickRename}
-          className="grw-page-control-dropdown-item"
-        >
-          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">redo</span>
-          {t('Rename')}
-        </DropdownItem>
 
-        <DropdownItem divider />
+          <DropdownItem divider />
 
-        <DropdownItem
-          className="pt-2 grw-page-control-dropdown-item text-danger"
-          onClick={onClickDelete}
-        >
-          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">delete</span>
-          {t('Delete')}
-        </DropdownItem>
-      </DropdownMenu>
+          <DropdownItem
+            className="pt-2 grw-page-control-dropdown-item text-danger"
+            onClick={onClickDelete}
+          >
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">delete</span>
+            {t('Delete')}
+          </DropdownItem>
+        </DropdownMenu>
+      ) }
     </Dropdown>
   );
 };

+ 12 - 9
apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -186,15 +186,18 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
       onToggle={toggleHandler}
     >
       {children}
-      <DropdownMenu
-        end
-        persist
-        strategy="fixed"
-        container="body"
-        className={`grw-bookmark-folder-menu ${styles['grw-bookmark-folder-menu']}`}
-      >
-        { renderBookmarkMenuItem() }
-      </DropdownMenu>
+
+      { isOpen && (
+        <DropdownMenu
+          end
+          persist
+          strategy="fixed"
+          container="body"
+          className={`grw-bookmark-folder-menu ${styles['grw-bookmark-folder-menu']}`}
+        >
+          { renderBookmarkMenuItem() }
+        </DropdownMenu>
+      ) }
     </UncontrolledDropdown>
   );
 };

+ 47 - 30
apps/app/src/client/components/Common/Dropdown/PageItemControl.spec.tsx

@@ -1,37 +1,54 @@
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
+import { type IPageInfoForOperation } from '@growi/core/dist/interfaces';
+import {
+  fireEvent, render, screen, within,
+} from '@testing-library/react';
+import { mock } from 'vitest-mock-extended';
 
 import { PageItemControl } from './PageItemControl';
 
 
+// mock for isIPageInfoForOperation
+
+const mocks = vi.hoisted(() => ({
+  isIPageInfoForOperationMock: vi.fn(),
+}));
+
+vi.mock('@growi/core/dist/interfaces', () => ({
+  isIPageInfoForOperation: mocks.isIPageInfoForOperationMock,
+}));
+
+
 describe('PageItemControl.tsx', () => {
-  it('Should trigger onClickRenameMenuItem() when clicking the rename button with pageInfo.isDeletable being "false"', async() => {
-    // setup
-    const onClickRenameMenuItemMock = vi.fn();
-
-    const pageInfo = {
-      isMovable: true,
-      isV5Compatible: true,
-      isEmpty: false,
-      isDeletable: false,
-      isAbleToDeleteCompletely: true,
-      isRevertible: true,
-    };
-
-    const props = {
-      pageId: 'dummy-page-id',
-      isEnableActions: true,
-      pageInfo,
-      onClickRenameMenuItem: onClickRenameMenuItemMock,
-    };
-
-    render(<PageItemControl {...props} />);
-
-    // when
-    const openPageMoveRenameModalButton = screen.getByTestId('rename-page-btn');
-    await waitFor(() => userEvent.click(openPageMoveRenameModalButton, { pointerEventsCheck: PointerEventsCheckLevel.Never }));
-
-    // then
-    expect(onClickRenameMenuItemMock).toHaveBeenCalled();
+  describe('Should trigger onClickRenameMenuItem() when clicking the rename button', () => {
+    it('without fetching PageInfo by useSWRxPageInfo', async() => {
+      // setup
+      const pageInfo = mock<IPageInfoForOperation>();
+
+      const onClickRenameMenuItemMock = vi.fn();
+      // return true when the argument is pageInfo in order to supress fetching
+      mocks.isIPageInfoForOperationMock.mockImplementation((arg) => {
+        if (arg === pageInfo) {
+          return true;
+        }
+      });
+
+      const props = {
+        pageId: 'dummy-page-id',
+        isEnableActions: true,
+        pageInfo,
+        onClickRenameMenuItem: onClickRenameMenuItemMock,
+      };
+
+      render(<PageItemControl {...props} />);
+
+      // when
+      const button = within(screen.getByTestId('open-page-item-control-btn')).getByText(/more_vert/);
+      fireEvent.click(button);
+      const renameMenuItem = await screen.findByTestId('rename-page-btn');
+      fireEvent.click(renameMenuItem);
+
+      // then
+      expect(onClickRenameMenuItemMock).toHaveBeenCalled();
+    });
   });
 });

+ 14 - 12
apps/app/src/client/components/Common/Dropdown/PageItemControl.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import {
   type IPageInfoAll, isIPageInfoForOperation,
-} from '@growi/core';
+} from '@growi/core/dist/interfaces';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import {
@@ -338,21 +338,23 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
     <NotAvailableForGuest>
       <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} className="grw-page-item-control" data-testid="open-page-item-control-btn">
         { children ?? (
-          <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
+          <DropdownToggle role="button" color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
             <span className="material-symbols-outlined">more_vert</span>
           </DropdownToggle>
         ) }
 
-        <PageItemControlDropdownMenu
-          {...props}
-          isLoading={isLoading}
-          pageInfo={fetchedPageInfo ?? presetPageInfo}
-          onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-          onClickRenameMenuItem={renameMenuItemClickHandler}
-          onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-          onClickDeleteMenuItem={deleteMenuItemClickHandler}
-          onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-        />
+        { isOpen && (
+          <PageItemControlDropdownMenu
+            {...props}
+            isLoading={isLoading}
+            pageInfo={fetchedPageInfo ?? presetPageInfo}
+            onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+            onClickRenameMenuItem={renameMenuItemClickHandler}
+            onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+            onClickDeleteMenuItem={deleteMenuItemClickHandler}
+            onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+          />
+        ) }
       </Dropdown>
 
     </NotAvailableForGuest>

+ 13 - 10
apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -86,18 +86,21 @@ export const InAppNotificationDropdown = (): JSX.Element => {
       <DropdownToggle className="px-3" color="primary" innerRef={buttonRef}>
         <span className="material-symbols-outlined">notifications</span> {badge}
       </DropdownToggle>
-      <DropdownMenu end>
-        { inAppNotificationData != null && inAppNotificationData.docs.length === 0
+
+      { isOpen && (
+        <DropdownMenu end>
+          { inAppNotificationData != null && inAppNotificationData.docs.length === 0
           // no items
-          ? <DropdownItem disabled>{t('in_app_notification.mark_all_as_read')}</DropdownItem>
+            ? <DropdownItem disabled>{t('in_app_notification.mark_all_as_read')}</DropdownItem>
           // render DropdownItem
-          : <InAppNotificationList inAppNotificationData={inAppNotificationData} />
-        }
-        <DropdownItem divider />
-        <DropdownItem tag="a" href="/me/all-in-app-notifications">
-          { t('in_app_notification.see_all') }
-        </DropdownItem>
-      </DropdownMenu>
+            : <InAppNotificationList inAppNotificationData={inAppNotificationData} />
+          }
+          <DropdownItem divider />
+          <DropdownItem tag="a" href="/me/all-in-app-notifications">
+            { t('in_app_notification.see_all') }
+          </DropdownItem>
+        </DropdownMenu>
+      ) }
     </Dropdown>
   );
 };

+ 6 - 2
apps/app/src/client/components/PageComment.tsx

@@ -93,8 +93,12 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
       onDeleteCommentAfterOperation();
     }
     catch (error: unknown) {
-      setErrorMessageOnDelete(error as string);
-      toastError(`error: ${error}`);
+      const message = error instanceof Error
+        ? error.message
+        : (error as any).toString();
+
+      setErrorMessageOnDelete(message);
+      toastError(message);
     }
   }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
 

+ 1 - 1
apps/app/src/client/components/PageComment/ReplyComments.tsx

@@ -69,7 +69,7 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 
   const areThereHiddenReplies = (replyList.length > 2);
   const toggleButtonIconName = isOlderRepliesShown ? 'expand_less' : 'more_vert';
-  const toggleButtonIcon = <span className="material-icons-outlined me-1">{toggleButtonIconName}</span>;
+  const toggleButtonIcon = <span className="material-symbols-outlined me-1">{toggleButtonIconName}</span>;
   const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
   const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
   const hiddenReplies = replyList.slice(0, replyList.length - 2);

+ 1 - 0
apps/app/src/interfaces/apiv3/page.ts

@@ -42,4 +42,5 @@ export type IApiv3PageUpdateResponse = {
 
 export const PageUpdateErrorCode = {
   CONFLICT: 'conflict',
+  FORBIDDEN: 'forbidden',
 } as const;

+ 0 - 14
apps/app/src/models/admin/growi-archive-import-option.js

@@ -1,14 +0,0 @@
-class GrowiArchiveImportOption {
-
-  constructor(collectionName, mode, initProps = {}) {
-    this.collectionName = collectionName;
-    this.mode = mode;
-
-    Object.entries(initProps).forEach(([key, value]) => {
-      this[key] = value;
-    });
-  }
-
-}
-
-module.exports = GrowiArchiveImportOption;

+ 18 - 0
apps/app/src/models/admin/growi-archive-import-option.ts

@@ -0,0 +1,18 @@
+import { ImportMode } from './import-mode';
+
+export class GrowiArchiveImportOption {
+
+  collectionName: string;
+
+  mode: ImportMode;
+
+  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = {}) {
+    this.collectionName = collectionName;
+    this.mode = mode;
+
+    Object.entries(initProps).forEach(([key, value]) => {
+      this[key] = value;
+    });
+  }
+
+}

+ 0 - 0
apps/app/src/server/service/import/import-mode.ts → apps/app/src/models/admin/import-mode.ts


+ 5 - 3
apps/app/src/models/admin/import-option-for-pages.ts

@@ -1,4 +1,6 @@
-import GrowiArchiveImportOption from './growi-archive-import-option';
+import { ImportMode } from '~/models/admin/import-mode';
+
+import { GrowiArchiveImportOption } from './growi-archive-import-option';
 
 const DEFAULT_PROPS = {
   isOverwriteAuthorWithCurrentUser: false,
@@ -20,8 +22,8 @@ export class ImportOptionForPages extends GrowiArchiveImportOption {
 
   initPageMetadatas;
 
-  constructor(collectionName: string, mode: string, initProps) {
-    super(collectionName, mode, initProps || DEFAULT_PROPS);
+  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = DEFAULT_PROPS) {
+    super(collectionName, mode, initProps);
   }
 
 }

+ 0 - 13
apps/app/src/models/admin/import-option-for-revisions.js

@@ -1,13 +0,0 @@
-const GrowiArchiveImportOption = require('./growi-archive-import-option');
-
-const DEFAULT_PROPS = {
-  isOverwriteAuthorWithCurrentUser: false,
-};
-
-export class ImportOptionForRevisions extends GrowiArchiveImportOption {
-
-  constructor(collectionName, mode, initProps) {
-    super(collectionName, mode, initProps || DEFAULT_PROPS);
-  }
-
-}

+ 15 - 0
apps/app/src/models/admin/import-option-for-revisions.ts

@@ -0,0 +1,15 @@
+import { ImportMode } from '~/models/admin/import-mode';
+
+import { GrowiArchiveImportOption } from './growi-archive-import-option';
+
+const DEFAULT_PROPS = {
+  isOverwriteAuthorWithCurrentUser: false,
+};
+
+export class ImportOptionForRevisions extends GrowiArchiveImportOption {
+
+  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = DEFAULT_PROPS) {
+    super(collectionName, mode, initProps);
+  }
+
+}

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

@@ -245,6 +245,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { pageWithMeta } = props;
 
   const pageId = pageWithMeta?.data._id;
+  const revisionId = pageWithMeta?.data.revision?._id;
   const revisionBody = pageWithMeta?.data.revision?.body;
 
   useCurrentPathname(props.currentPathname);
@@ -277,7 +278,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       return;
     }
 
-    if (currentPageId != null && !props.isNotFound) {
+    if (currentPageId != null && revisionId != null && !props.isNotFound) {
       const mutatePageData = async() => {
         const pageData = await mutateCurrentPage();
         mutateEditingMarkdown(pageData?.revision?.body);
@@ -288,7 +289,10 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       // Because pageWIthMeta does not contain revision.body
       mutatePageData();
     }
-  }, [currentPageId, mutateCurrentPage, mutateCurrentPageYjsDataFromApi, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);
+  }, [
+    revisionId, currentPageId, mutateCurrentPage,
+    mutateCurrentPageYjsDataFromApi, mutateEditingMarkdown, props.isNotFound, props.skipSSR,
+  ]);
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
@@ -308,8 +312,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
 
   useEffect(() => {
-    mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
-  }, [mutateRemoteRevisionId, pageWithMeta?.data.revision?._id]);
+    mutateRemoteRevisionId(revisionId);
+  }, [mutateRemoteRevisionId, revisionId]);
 
   useEffect(() => {
     mutateCurrentPageId(pageId ?? null);

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

@@ -50,6 +50,7 @@ export interface PageDocument extends IPage, Document<Types.ObjectId> {
   [x:string]: any // for obsolete methods
   getLatestRevisionBodyLength(): Promise<number | null | undefined>
   calculateAndUpdateLatestRevisionBodyLength(this: PageDocument): Promise<void>
+  populateDataToShowRevision(shouldExcludeBody?: boolean): Promise<PageDocument>
 }
 
 

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

@@ -268,7 +268,7 @@ export default function route(crowi) {
     const importSettingsMap = {};
     fileStatsToImport.forEach(({ fileName, collectionName }) => {
       // instanciate GrowiArchiveImportOption
-      /** @type {GrowiArchiveImportOption} */
+      /** @type {import('~/models/admin/growi-archive-import-option').GrowiArchiveImportOption} */
       const option = options.find(opt => opt.collectionName === collectionName);
 
       // generate options

+ 15 - 5
apps/app/src/server/routes/apiv3/page/index.ts

@@ -20,6 +20,7 @@ import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { Revision } from '~/server/models/revision';
+import ShareLink from '~/server/models/share-link';
 import Subscription from '~/server/models/subscription';
 import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
@@ -202,6 +203,7 @@ module.exports = (crowi) => {
       query('pageId').optional().isString(),
       query('path').optional().isString(),
       query('findAll').optional().isBoolean(),
+      query('shareLinkId').optional().isMongoId(),
     ],
     likes: [
       body('pageId').isString(),
@@ -284,19 +286,27 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/Page'
    */
   router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
-    const { user } = req;
+    const { user, isSharedPage } = req;
     const {
-      pageId, path, findAll, revisionId,
+      pageId, path, findAll, revisionId, shareLinkId,
     } = req.query;
 
-    if (pageId == null && path == null) {
-      return res.apiv3Err(new ErrorV3('Either parameter of path or pageId is required.', 'invalid-request'));
+    const isValid = (shareLinkId != null && pageId != null && path == null) || (shareLinkId == null && (pageId != null || path != null));
+    if (!isValid) {
+      return res.apiv3Err(new Error('Either parameter of (pageId or path) or (pageId and shareLinkId) is required.'), 400);
     }
 
     let page;
     let pages;
     try {
-      if (pageId != null) { // prioritized
+      if (isSharedPage) {
+        const shareLink = await ShareLink.findOne({ _id: shareLinkId });
+        if (shareLink == null) {
+          throw new Error('ShareLink is not found');
+        }
+        page = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+      }
+      else if (pageId != null) { // prioritized
         page = await Page.findByIdAndViewer(pageId, user);
       }
       else if (!findAll) {

+ 10 - 2
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -4,6 +4,7 @@ import type {
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
@@ -27,6 +28,7 @@ import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '../../../middlewares/exclude-read-only-user';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:update-page');
 
 
@@ -121,7 +123,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
       const {
-        pageId, revisionId, body, origin,
+        pageId, revisionId, body, origin, grant,
       } = req.body;
 
       const sanitizeRevisionId = revisionId == null ? undefined : generalXssFilter.process(revisionId);
@@ -139,6 +141,12 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         return res.apiv3Err(new ErrorV3(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 400);
       }
 
+      const isGrantImmutable = isTopPage(currentPage.path) || isUsersProtectedPages(currentPage.path);
+
+      if (grant != null && grant !== currentPage.grant && isGrantImmutable) {
+        return res.apiv3Err(new ErrorV3('The grant settings for the specified page cannot be modified.', PageUpdateErrorCode.FORBIDDEN), 403);
+      }
+
       if (currentPage != null) {
         // Normalize the latest revision which was borken by the migration script '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js'
         try {
@@ -164,7 +172,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
       let previousRevision: IRevisionHasId | null;
       try {
         const {
-          grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
+          userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
         } = req.body;
         const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin, wip };
         if (grant != null) {

+ 4 - 3
apps/app/src/server/routes/comment.js

@@ -1,4 +1,5 @@
 
+import { getIdStringForRef } from '@growi/core';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 import { Comment, CommentEvent, commentEvent } from '~/features/comment/server';
@@ -56,7 +57,6 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:comment');
   const User = crowi.model('User');
   const Page = crowi.model('Page');
-  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const ApiResponse = require('../util/apiResponse');
 
   const activityEvent = crowi.event('activity');
@@ -465,6 +465,7 @@ module.exports = function(crowi, app) {
     }
 
     try {
+      /** @type {import('mongoose').HydratedDocument<import('~/interfaces/comment').IComment>} */
       const comment = await Comment.findById(commentId).exec();
 
       if (comment == null) {
@@ -472,12 +473,12 @@ module.exports = function(crowi, app) {
       }
 
       // check whether accessible
-      const pageId = comment.page;
+      const pageId = getIdStringForRef(comment.page);
       const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
-      if (req.user._id !== comment.creator.toString()) {
+      if (getIdStringForRef(req.user) !== getIdStringForRef(comment.creator)) {
         throw new Error('Current user is not operatable to this comment.');
       }
 

+ 5 - 4
apps/app/src/server/service/g2g-transfer.ts

@@ -10,9 +10,10 @@ import FormData from 'form-data';
 import mongoose, { Types as MongooseTypes } from 'mongoose';
 
 import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
+import { ImportMode } from '~/models/admin/import-mode';
 import TransferKeyModel from '~/server/models/transfer-key';
-import { getImportService, ImportMode, type ImportSettings } from '~/server/service/import';
+import { getImportService, type ImportSettings } from '~/server/service/import';
 import { createBatchStream } from '~/server/util/batch-stream';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
@@ -609,12 +610,12 @@ export class G2GTransferReceiverService implements Receiver {
   ): { [key: string]: ImportSettings; } {
     const importSettingsMap = {};
     innerFileStats.forEach(({ fileName, collectionName }) => {
-      const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
+      const options = new GrowiArchiveImportOption(collectionName, undefined, optionsMap[collectionName]);
 
       if (collectionName === 'configs' && options.mode !== ImportMode.flushAndInsert) {
         throw new Error('`flushAndInsert` is only available as an import setting for configs collection');
       }
-      if (collectionName === 'pages' && options.mode === 'insert') {
+      if (collectionName === 'pages' && options.mode === ImportMode.insert) {
         throw new Error('`insert` is not available as an import setting for pages collection');
       }
       if (collectionName === 'attachmentFiles.chunks') {

+ 2 - 1
apps/app/src/server/service/import/import-settings.ts

@@ -1,4 +1,5 @@
-import type { ImportMode } from './import-mode';
+import type { ImportMode } from '~/models/admin/import-mode';
+
 import type { OverwriteFunction } from './overwrite-function';
 
 export type OverwriteParams = { [propertyName: string]: OverwriteFunction | unknown }

+ 5 - 7
apps/app/src/server/service/import/import.ts

@@ -13,6 +13,7 @@ import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 import unzipStream from 'unzip-stream';
 
+import { ImportMode } from '~/models/admin/import-mode';
 import type Crowi from '~/server/crowi';
 import { setupIndependentModels } from '~/server/crowi/setup-models';
 import type CollectionProgress from '~/server/models/vo/collection-progress';
@@ -25,7 +26,6 @@ import { configManager } from '../config-manager';
 import type { ConvertMap } from './construct-convert-map';
 import { constructConvertMap } from './construct-convert-map';
 import { getModelFromCollectionName } from './get-model-from-collection-name';
-import { ImportMode } from './import-mode';
 import type { ImportSettings, OverwriteParams } from './import-settings';
 import { keepOriginal } from './overwrite-function';
 
@@ -303,14 +303,12 @@ export class ImportService {
 
   /**
    * process bulk operation
-   * @param {object} bulk MongoDB Bulk instance
-   * @param {string} collectionName collection name
-   * @param {object} document
-   * @param {ImportSettings} importSettings
+   * @param bulk MongoDB Bulk instance
+   * @param collectionName collection name
    */
-  bulkOperate(bulk, collectionName, document, importSettings) {
+  bulkOperate(bulk, collectionName: string, document, importSettings: ImportSettings) {
     // insert
-    if (importSettings.mode !== 'upsert') {
+    if (importSettings.mode !== ImportMode.upsert) {
       return bulk.insert(document);
     }
 

+ 0 - 2
apps/app/src/server/service/import/index.ts

@@ -18,6 +18,4 @@ export const getImportService = (): ImportService => {
   return instance;
 };
 
-
-export * from './import-mode';
 export * from './import-settings';

+ 1 - 1
apps/app/src/server/service/import/overwrite-params/index.ts

@@ -1,4 +1,4 @@
-import type GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { isImportOptionForPages } from '~/models/admin/import-option-for-pages';
 
 import type { OverwriteParams } from '../import-settings';

+ 14 - 2
apps/app/src/stores/page.tsx

@@ -57,10 +57,14 @@ export const useTemplateBodyData = (initialData?: string): SWRResponse<string, E
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
 
+  const { data: isLatestRevision } = useIsLatestRevision();
+
   const { cache } = useSWRConfig();
 
   // Problem 1: https://github.com/weseek/growi/pull/7772/files#diff-4c1708c4f959974166c15435c6b35950ba01bbf35e7e4b8e99efeb125a8000a7
   // Problem 2: https://redmine.weseek.co.jp/issues/141027
+  // Problem 3: https://redmine.weseek.co.jp/issues/153618
+  // Problem 4: https://redmine.weseek.co.jp/issues/153759
   const shouldMutate = (() => {
     if (initialData === undefined) {
       return false;
@@ -81,6 +85,14 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
       return true;
     }
 
+    // mutate when opening a previous revision.
+    if (!isLatestRevision
+        && cachedData.revision?._id != null && initialData.revision?._id != null
+        && cachedData.revision._id !== initialData.revision._id
+    ) {
+      return true;
+    }
+
     return false;
   })();
 
@@ -280,7 +292,7 @@ export const useSWRxCurrentGrantData = (
     ? ['/page/grant-data', pageId]
     : null;
 
-  return useSWRImmutable(
+  return useSWR(
     key,
     ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
@@ -290,7 +302,7 @@ export const useSWRxApplicableGrant = (
     pageId: string | null | undefined,
 ): SWRResponse<IRecordApplicableGrant, Error> => {
 
-  return useSWRImmutable(
+  return useSWR(
     pageId != null ? ['/page/applicable-grant', pageId] : null,
     ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );

+ 3 - 2
apps/app/turbo.json

@@ -49,8 +49,9 @@
       "cache": false,
       "persistent": true
     },
-    "dev:ci": {
-      "dependsOn": ["^dev", "dev:migrate", "dev:styles-prebuilt"],
+
+    "launch-dev:ci": {
+      "dependsOn": ["^dev", "dev:styles-prebuilt"],
       "cache": false
     },
 

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

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

+ 3 - 1
apps/slackbot-proxy/src/services/LinkSharedService.ts

@@ -3,6 +3,8 @@ import type { WebClient } from '@slack/web-api';
 import { Inject, Service } from '@tsed/di';
 import axios from 'axios';
 
+// needed to import class (not type) for injection
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import { RelationRepository } from '~/repositories/relation';
 import loggerFactory from '~/utils/logger';
 
@@ -42,7 +44,7 @@ type PublicData = {
 export type DataForLinkShared = PrivateData | PublicData;
 
 @Service()
-export class LinkSharedService implements GrowiEventProcessor {
+export class LinkSharedService implements GrowiEventProcessor<LinkSharedRequestEvent> {
 
   @Inject()
   relationRepository: RelationRepository;

+ 42 - 0
apps/slackbot-proxy/turbo.json

@@ -0,0 +1,42 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+
+    "clean": {
+      "dependsOn": ["@growi/slack#clean"],
+      "cache": false
+    },
+
+    "build": {
+      "dependsOn": ["@growi/slack#build"],
+      "outputs": ["dist/**"],
+      "outputLogs": "new-only"
+    },
+
+    "dev": {
+      "dependsOn": ["@growi/slack#dev"],
+      "cache": false,
+      "persistent": true
+    },
+    "dev:ci": {
+      "dependsOn": ["@growi/slack#dev"],
+      "cache": false
+    },
+
+    "lint": {
+      "dependsOn": ["@growi/slack#dev"]
+    },
+
+    "test": {
+      "dependsOn": ["@growi/slack#dev"],
+      "outputLogs": "new-only"
+    },
+
+    "version": {
+      "cache": false,
+      "dependsOn": ["^version", "//#version"]
+    }
+
+  }
+}

+ 1 - 1
package.json

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

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

@@ -49,7 +49,7 @@
     "@growi/ui": "link:../ui",
     "axios": "^0.24.0",
     "bunyan": "^1.8.15",
-    "express": "^4.19.2",
+    "express": "^4.20.0",
     "hast-util-select": "^5.0.5",
     "mongoose": "^6.11.3",
     "swr": "^2.0.3",

+ 1 - 1
packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts

@@ -1,7 +1,7 @@
 import type { PhrasingContent } from 'mdast';
 import type { Parent } from 'unist';
 
-import { DirectiveType } from './consts.js';
+import type { DirectiveType } from './consts.js';
 
 
 type DirectiveAttributes = Record<string, string>

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

@@ -37,7 +37,7 @@
     "@growi/remark-growi-directive": "link:../remark-growi-directive",
     "@growi/ui": "link:../ui",
     "escape-string-regexp": "^4.0.0",
-    "express": "^4.19.2",
+    "express": "^4.20.0",
     "http-errors": "^2.0.0",
     "mongoose": "^6.11.3",
     "swr": "^2.2.2"

+ 2 - 2
packages/slack/src/interfaces/growi-event-processor.ts

@@ -1,7 +1,7 @@
 import type { WebClient } from '@slack/web-api';
 
-export interface GrowiEventProcessor {
+export interface GrowiEventProcessor<EVENT> {
   shouldHandleEvent(eventType: string): boolean;
 
-  processEvent(client: WebClient, event: any): Promise<void>;
+  processEvent(client: WebClient, event: EVENT): Promise<void>;
 }

+ 0 - 21
turbo.json

@@ -34,11 +34,6 @@
       "outputs": ["dist/**"],
       "outputLogs": "new-only"
     },
-    "@growi/slackbot-proxy#build": {
-      "dependsOn": ["@growi/slack#build"],
-      "outputs": ["dist/**"],
-      "outputLogs": "new-only"
-    },
     "build": {
       "outputs": ["dist/**"],
       "inputs": [
@@ -66,15 +61,6 @@
       "outputs": ["dist/**"],
       "outputLogs": "new-only"
     },
-    "@growi/slackbot-proxy#dev": {
-      "dependsOn": ["@growi/slack#dev"],
-      "cache": false,
-      "persistent": true
-    },
-    "@growi/slackbot-proxy#dev:ci": {
-      "dependsOn": ["@growi/slack#dev"],
-      "cache": false
-    },
     "dev": {
       "outputs": ["dist/**"],
       "inputs": [
@@ -108,16 +94,9 @@
     "@growi/ui#lint": {
       "dependsOn": ["@growi/core#dev"]
     },
-    "@growi/slackbot-proxy#lint": {
-      "dependsOn": ["@growi/slack#dev"]
-    },
     "lint": {
     },
 
-    "@growi/slackbot-proxy#test": {
-      "dependsOn": ["@growi/slack#dev"],
-      "outputLogs": "new-only"
-    },
     "@growi/preset-templates#test": {
       "dependsOn": ["@growi/pluginkit#dev"],
       "outputLogs": "new-only"

+ 163 - 42
yarn.lock

@@ -2146,7 +2146,7 @@
     "@growi/ui" "link:packages/ui"
     axios "^0.24.0"
     bunyan "^1.8.15"
-    express "^4.19.2"
+    express "^4.20.0"
     hast-util-select "^5.0.5"
     mongoose "^6.11.3"
     swr "^2.0.3"
@@ -2180,7 +2180,7 @@
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
     "@growi/ui" "link:packages/ui"
     escape-string-regexp "^4.0.0"
-    express "^4.19.2"
+    express "^4.20.0"
     http-errors "^2.0.0"
     mongoose "^6.11.3"
     swr "^2.2.2"
@@ -5855,10 +5855,10 @@ bn.js@^4.0.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
 
-body-parser@1.20.2, body-parser@^1.18.2:
-  version "1.20.2"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
-  integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
+body-parser@1.20.3, body-parser@^1.20.3:
+  version "1.20.3"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
+  integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
   dependencies:
     bytes "3.1.2"
     content-type "~1.0.5"
@@ -5868,7 +5868,7 @@ body-parser@1.20.2, body-parser@^1.18.2:
     http-errors "2.0.0"
     iconv-lite "0.4.24"
     on-finished "2.4.1"
-    qs "6.11.0"
+    qs "6.13.0"
     raw-body "2.5.2"
     type-is "~1.6.18"
     unpipe "1.0.0"
@@ -8082,6 +8082,11 @@ encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
 
+encodeurl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+  integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+
 encoding-down@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b"
@@ -8819,37 +8824,74 @@ express-validator@^6.14.0:
     lodash "^4.17.21"
     validator "^13.7.0"
 
-express@^4.17.1, express@^4.19.2:
-  version "4.19.2"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
-  integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
+express@^4.17.1:
+  version "4.21.0"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915"
+  integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==
   dependencies:
     accepts "~1.3.8"
     array-flatten "1.1.1"
-    body-parser "1.20.2"
+    body-parser "1.20.3"
     content-disposition "0.5.4"
     content-type "~1.0.4"
     cookie "0.6.0"
     cookie-signature "1.0.6"
     debug "2.6.9"
     depd "2.0.0"
-    encodeurl "~1.0.2"
+    encodeurl "~2.0.0"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    finalhandler "1.3.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    merge-descriptors "1.0.3"
+    methods "~1.1.2"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    path-to-regexp "0.1.10"
+    proxy-addr "~2.0.7"
+    qs "6.13.0"
+    range-parser "~1.2.1"
+    safe-buffer "5.2.1"
+    send "0.19.0"
+    serve-static "1.16.2"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    type-is "~1.6.18"
+    utils-merge "1.0.1"
+    vary "~1.1.2"
+
+express@^4.20.0:
+  version "4.20.0"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48"
+  integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==
+  dependencies:
+    accepts "~1.3.8"
+    array-flatten "1.1.1"
+    body-parser "1.20.3"
+    content-disposition "0.5.4"
+    content-type "~1.0.4"
+    cookie "0.6.0"
+    cookie-signature "1.0.6"
+    debug "2.6.9"
+    depd "2.0.0"
+    encodeurl "~2.0.0"
     escape-html "~1.0.3"
     etag "~1.8.1"
     finalhandler "1.2.0"
     fresh "0.5.2"
     http-errors "2.0.0"
-    merge-descriptors "1.0.1"
+    merge-descriptors "1.0.3"
     methods "~1.1.2"
     on-finished "2.4.1"
     parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
+    path-to-regexp "0.1.10"
     proxy-addr "~2.0.7"
     qs "6.11.0"
     range-parser "~1.2.1"
     safe-buffer "5.2.1"
-    send "0.18.0"
-    serve-static "1.15.0"
+    send "0.19.0"
+    serve-static "1.16.0"
     setprototypeof "1.2.0"
     statuses "2.0.1"
     type-is "~1.6.18"
@@ -9085,6 +9127,19 @@ finalhandler@1.2.0:
     statuses "2.0.1"
     unpipe "~1.0.0"
 
+finalhandler@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
+  integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~2.0.0"
+    escape-html "~1.0.3"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    statuses "2.0.1"
+    unpipe "~1.0.0"
+
 find-cache-dir@^3.3.1, find-cache-dir@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
@@ -11399,9 +11454,9 @@ jmespath@0.15.0:
   resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
 
 jose@^4.10.0:
-  version "4.11.4"
-  resolved "https://registry.yarnpkg.com/jose/-/jose-4.11.4.tgz#e04d4a393ac017d0450fa0a38e2f2382cd73f71e"
-  integrity sha512-94FdcR8felat4vaTJyL/WVdtlWLlsnLMZP8v+A0Vru18K3bQ22vn7TtpVh3JlgBFNIlYOUlGqwp/MjRPOnIyCQ==
+  version "4.15.9"
+  resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100"
+  integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==
 
 jpeg-js@^0.4.0, jpeg-js@^0.4.2:
   version "0.4.4"
@@ -12623,9 +12678,10 @@ meow@^6.0.0:
     type-fest "^0.13.1"
     yargs-parser "^18.1.3"
 
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+merge-descriptors@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
+  integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
 
 merge-stream@^2.0.0:
   version "2.0.0"
@@ -13710,10 +13766,10 @@ nodemailer-ses-transport@~1.5.0:
   dependencies:
     aws-sdk "^2.2.36"
 
-nodemailer@^6.9.14:
-  version "6.9.14"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
-  integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
+nodemailer@^6.9.15:
+  version "6.9.15"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04"
+  integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==
 
 nodemon@^3.1.3:
   version "3.1.3"
@@ -14498,9 +14554,10 @@ path-scurry@^1.10.1:
     lru-cache "^9.1.1 || ^10.0.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
 
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+path-to-regexp@0.1.10:
+  version "0.1.10"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
+  integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
 
 path-type@^1.0.0:
   version "1.1.0"
@@ -14915,12 +14972,12 @@ qs@6.11.0:
   dependencies:
     side-channel "^1.0.4"
 
-qs@^6.10.2, qs@^6.11.1:
-  version "6.11.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f"
-  integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==
+qs@6.13.0, qs@^6.10.2, qs@^6.11.1:
+  version "6.13.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+  integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
   dependencies:
-    side-channel "^1.0.4"
+    side-channel "^1.0.6"
 
 qs@~6.5.2:
   version "6.5.2"
@@ -16225,6 +16282,25 @@ send@0.18.0:
     range-parser "~1.2.1"
     statuses "2.0.1"
 
+send@0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
+  integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
+  dependencies:
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    mime "1.6.0"
+    ms "2.1.3"
+    on-finished "2.4.1"
+    range-parser "~1.2.1"
+    statuses "2.0.1"
+
 sentence-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f"
@@ -16239,16 +16315,26 @@ seq-queue@^0.0.5:
   resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
   integrity sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=
 
-serve-static@1.15.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
-  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+serve-static@1.16.0:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92"
+  integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==
   dependencies:
     encodeurl "~1.0.2"
     escape-html "~1.0.3"
     parseurl "~1.3.3"
     send "0.18.0"
 
+serve-static@1.16.2:
+  version "1.16.2"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
+  integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
+  dependencies:
+    encodeurl "~2.0.0"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.19.0"
+
 set-blocking@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -16411,6 +16497,16 @@ side-channel@^1.0.3, side-channel@^1.0.4:
     get-intrinsic "^1.0.2"
     object-inspect "^1.9.0"
 
+side-channel@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
+  integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+  dependencies:
+    call-bind "^1.0.7"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.4"
+    object-inspect "^1.13.1"
+
 sift@16.0.1:
   version "16.0.1"
   resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053"
@@ -16927,7 +17023,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@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
   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==
@@ -16945,6 +17041,15 @@ 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"
@@ -17028,7 +17133,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@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm: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==
@@ -17042,6 +17147,13 @@ 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"
@@ -18828,7 +18940,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@^7.0.0:
+"wrap-ansi-cjs@npm: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==
@@ -18846,6 +18958,15 @@ 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"