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

Merge pull request #9047 from weseek/master

Release v7.0.18
mergify[bot] 1 год назад
Родитель
Сommit
cc1a3de21e
77 измененных файлов с 862 добавлено и 1803 удалено
  1. 45 0
      .github/mergify.yml
  2. 3 1
      .github/workflows/auto-labeling.yml
  3. 14 22
      .github/workflows/ci-app-prod.yml
  4. 1 0
      .github/workflows/ci-app.yml
  5. 4 0
      .github/workflows/ci-slackbot-proxy.yml
  6. 1 146
      .github/workflows/reusable-app-prod.yml
  7. 0 24
      .mergify.yml
  8. 0 2
      apps/app/.gitignore
  9. 0 30
      apps/app/cypress.config.ts
  10. 2 5
      apps/app/package.json
  11. 12 11
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  12. 170 0
      apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts
  13. 43 0
      apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts
  14. 0 2
      apps/app/playwright/utils/CollapseSidebar.ts
  15. 1 1
      apps/app/regconfig.json
  16. 3 3
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  17. 1 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  18. 1 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  19. 2 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  20. 28 25
      apps/app/src/client/components/Bookmarks/BookmarkFolderItemControl.tsx
  21. 12 9
      apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.tsx
  22. 47 30
      apps/app/src/client/components/Common/Dropdown/PageItemControl.spec.tsx
  23. 14 12
      apps/app/src/client/components/Common/Dropdown/PageItemControl.tsx
  24. 13 10
      apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx
  25. 1 1
      apps/app/src/client/components/PageComment/ReplyComments.tsx
  26. 1 1
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  27. 16 11
      apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx
  28. 0 36
      apps/app/src/client/components/SearchForm.module.scss
  29. 0 143
      apps/app/src/client/components/SearchForm.tsx
  30. 1 1
      apps/app/src/client/components/Sidebar/Tag.tsx
  31. 0 14
      apps/app/src/models/admin/growi-archive-import-option.js
  32. 18 0
      apps/app/src/models/admin/growi-archive-import-option.ts
  33. 0 0
      apps/app/src/models/admin/import-mode.ts
  34. 5 3
      apps/app/src/models/admin/import-option-for-pages.ts
  35. 0 13
      apps/app/src/models/admin/import-option-for-revisions.js
  36. 15 0
      apps/app/src/models/admin/import-option-for-revisions.ts
  37. 8 4
      apps/app/src/pages/[[...path]].page.tsx
  38. 23 5
      apps/app/src/server/models/page.ts
  39. 2 2
      apps/app/src/server/models/user-group-relation.ts
  40. 1 1
      apps/app/src/server/routes/apiv3/import.js
  41. 8 1
      apps/app/src/server/routes/apiv3/page-listing.ts
  42. 3 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  43. 9 0
      apps/app/src/server/routes/apiv3/pages/index.js
  44. 5 4
      apps/app/src/server/service/g2g-transfer.ts
  45. 2 1
      apps/app/src/server/service/import/import-settings.ts
  46. 5 7
      apps/app/src/server/service/import/import.ts
  47. 0 2
      apps/app/src/server/service/import/index.ts
  48. 1 1
      apps/app/src/server/service/import/overwrite-params/index.ts
  49. 8 3
      apps/app/src/server/service/page/index.ts
  50. 3 1
      apps/app/src/server/service/page/page-service.ts
  51. 1 1
      apps/app/src/server/util/compare-objectId.ts
  52. 5 0
      apps/app/src/stores/page.tsx
  53. 18 4
      apps/app/src/stores/ui.tsx
  54. 0 8
      apps/app/test/cypress/.eslintrc.js
  55. 0 102
      apps/app/test/cypress/e2e/0-advanced-examples/misc.cy.ts
  56. 0 59
      apps/app/test/cypress/e2e/0-advanced-examples/viewport.cy.ts
  57. 0 18
      apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts
  58. 0 303
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  59. 0 65
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts
  60. 0 6
      apps/app/test/cypress/fixtures/user-admin.json
  61. 0 21
      apps/app/test/cypress/support/assertions.ts
  62. 0 14
      apps/app/test/cypress/support/blackout.ts
  63. 0 118
      apps/app/test/cypress/support/commands.ts
  64. 0 48
      apps/app/test/cypress/support/index.ts
  65. 0 9
      apps/app/test/cypress/support/screenshot.ts
  66. 0 16
      apps/app/test/cypress/tsconfig.json
  67. 1 1
      apps/slackbot-proxy/package.json
  68. 3 1
      apps/slackbot-proxy/src/services/LinkSharedService.ts
  69. 42 0
      apps/slackbot-proxy/turbo.json
  70. 0 53
      bin/github-actions/generate-cypress-spec-arg.mjs
  71. 1 3
      package.json
  72. 1 0
      packages/editor/src/client/components/CodeMirrorEditorReadOnly.tsx
  73. 1 1
      packages/editor/src/client/stores/codemirror-editor.ts
  74. 1 1
      packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts
  75. 2 2
      packages/slack/src/interfaces/growi-event-processor.ts
  76. 0 21
      turbo.json
  77. 234 336
      yarn.lock

+ 45 - 0
.github/mergify.yml

@@ -0,0 +1,45 @@
+queue_rules:
+  - name: default
+    allow_inplace_checks: false
+    queue_conditions:
+      - '#check-failure = 0'
+      - or:
+        - and:
+            - check-success ~= ci-slackbot-proxy-
+        - and:
+            - check-success ~= ci-app-
+    merge_conditions:
+      - '#check-failure = 0'
+      - or:
+        - and:
+            - check-success ~= ci-slackbot-proxy-
+        - and:
+            - check-success ~= ci-app-
+            - check-success ~= test-prod-node20 /
+
+pull_request_rules:
+  - name: Automatic queue to merge
+    conditions:
+      - '#approved-reviews-by >= 1'
+      - '#review-requested = 0'
+      - check-success = "check-title"
+    actions:
+      queue:
+
+  - name: Automatic merge for Dependabot pull requests
+    conditions:
+      - author = dependabot[bot]
+      - '#approved-reviews-by >= 1'
+      - '#check-failure = 0'
+      - check-success = "check-title"
+    actions:
+      merge:
+        method: merge
+
+  - name: Automatic merge for Preparing next version
+    conditions:
+      - author = github-actions[bot]
+      - label = "type/prepare-next-version"
+    actions:
+      merge:
+        method: merge

+ 3 - 1
.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,6 +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, 'mergify/merge-queue/' )
         && !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:

+ 14 - 22
.github/workflows/ci-app-prod.yml

@@ -7,6 +7,7 @@ on:
       - dev/7.*.x
       - dev/6.*.x
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
@@ -24,6 +25,7 @@ on:
       - dev/6.*.x
     types: [opened, reopened, synchronize]
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
@@ -34,12 +36,6 @@ on:
       - apps/app/**
       - '!apps/app/docker/**'
       - packages/**
-  workflow_call:
-    inputs:
-      cypress-config-video:
-        description: 'Enable video when running Cypress test'
-        type: boolean
-        default: false
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -62,25 +58,21 @@ jobs:
     with:
       node-version: 20.x
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name-prefix: cypress-report-
-      cypress-config-video: ${{ inputs.cypress-config-video || false }}
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
+  # run-reg-suit-node20:
+  #   needs: [test-prod-node20]
 
-  run-reg-suit-node20:
-    needs: [test-prod-node20]
+  #   uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
 
-    uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+  #   if: always()
 
-    if: always()
-
-    with:
-      node-version: 20.x
-      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name-pattern: cypress-report-*
-    secrets:
-      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
-      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  #   with:
+  #     node-version: 20.x
+  #     skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
+  #   secrets:
+  #     REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+  #     AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+  #     AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+  #     SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 1 - 0
.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

+ 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 - 146
.github/workflows/reusable-app-prod.yml

@@ -8,11 +8,6 @@ on:
         type: string
       skip-e2e-test:
         type: boolean
-      cypress-report-artifact-name-prefix:
-        type: string
-      cypress-config-video:
-        type: boolean
-        default: false
     secrets:
       SLACK_WEBHOOK_URL:
         required: true
@@ -201,150 +196,10 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-
-  run-cypress:
-    needs: [build-prod]
-
-    if: ${{ !inputs.skip-e2e-test }}
-
-    runs-on: ubuntu-latest
-
-    strategy:
-      fail-fast: false
-      matrix:
-        # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['21', '50']
-
-    services:
-      mongodb:
-        image: mongo:6.0
-        ports:
-        - 27017/tcp
-      elasticsearch:
-        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
-        ports:
-        - 9200/tcp
-        env:
-          discovery.type: single-node
-
-    steps:
-    - uses: actions/checkout@v4
-
-    - name: Install fonts
-      run: sudo apt install fonts-noto
-
-    - uses: actions/setup-node@v4
-      with:
-        node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Install turbo
-      run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune @growi/app
-        rm -rf apps packages
-        mv out/* .
-
-    - name: Restore node_modules
-      uses: actions/cache/restore@v4
-      with:
-        path: |
-          **/node_modules
-        # saved key by build-prod
-        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Cache/Restore Cypress files
-      uses: actions/cache@v4
-      with:
-        path: |
-          ~/.cache/Cypress
-        key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Install dependencies
-      run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
-        yarn cypress install
-
-    - name: Download production files artifact
-      uses: actions/download-artifact@v4
-      with:
-        name: Production Files (node${{ inputs.node-version }})
-
-    - name: Extract procution files artifact
-      run: |
-        tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
-
-    - name: Determine spec expression
-      id: determine-spec-exp
-      run: |
-        SPEC=`node bin/github-actions/generate-cypress-spec-arg.mjs --prefix="test/cypress/e2e/" --suffix="-*/*.cy.{ts,tsx}" "${{ matrix.spec-group }}"`
-        echo "value=$SPEC" >> $GITHUB_OUTPUT
-
-    - name: Copy dotenv file for ci
-      working-directory: ./apps/app
-      run: |
-        cat config/ci/.env.local.for-ci >> .env.production.local
-
-    - name: Copy dotenv file for automatic installation
-      if: ${{ matrix.spec-group != '10' }}
-      working-directory: ./apps/app
-      run: |
-        cat config/ci/.env.local.for-auto-install >> .env.production.local
-
-    - name: Copy dotenv file for automatic installation with allowing guest mode
-      if: ${{ matrix.spec-group == '21' }}
-      working-directory: ./apps/app
-      run: |
-        cat config/ci/.env.local.for-auto-install-with-allowing-guest >> .env.production.local
-
-    - name: Cypress Run
-      uses: cypress-io/github-action@v6
-      with:
-        browser: chromium
-        working-directory: ./apps/app
-        spec: '${{ steps.determine-spec-exp.outputs.value }}'
-        install: false
-        start: yarn server
-        wait-on: 'http://localhost:3000'
-        config: video=${{ inputs.cypress-config-video }}
-      env:
-        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
-        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
-
-    - name: Upload results
-      if: always()
-      uses: actions/upload-artifact@v4
-      with:
-        name: ${{ inputs.cypress-report-artifact-name-prefix }}${{ matrix.spec-group }}
-        path: |
-          apps/app/test/cypress/screenshots
-          apps/app/test/cypress/videos
-
-    - name: Slack Notification
-      uses: weseek/ghaction-slack-notification@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*Node CI for growi - run-cypress (${{ inputs.node-version }})*'
-        channel: '#ci'
-        isCompactMode: true
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-
-
   run-playwright:
     needs: [build-prod]
 
-    if: ${{ !inputs.skip-e2e-test }}
+    if: ${{ !inputs.skip-e2e-test && startsWith(github.head_ref, 'mergify/merge-queue/') }}
 
     runs-on: ubuntu-latest
     container:

+ 0 - 24
.mergify.yml

@@ -1,24 +0,0 @@
-pull_request_rules:
-  - name: Automatic merge for Dependabot pull requests
-    conditions:
-      - author = dependabot[bot]
-      - '#approved-reviews-by >= 1'
-      - check-success = "ci-slackbot-proxy-lint (20.x)"
-      - check-success = "ci-slackbot-proxy-launch-dev (20.x)"
-      - check-success = "ci-slackbot-proxy-launch-prod (20.x)"
-      - check-success = "ci-app-lint (20.x)"
-      - check-success = "ci-app-test (20.x)"
-      - check-success = "ci-app-launch-dev (20.x)"
-      - check-success = "test-prod-node18 / launch-prod"
-      - check-success = "test-prod-node20 / launch-prod"
-    actions:
-      merge:
-        method: merge
-
-  - name: Automatic merge for Preparing next version
-    conditions:
-      - author = github-actions[bot]
-      - label = "type/prepare-next-version"
-    actions:
-      merge:
-        method: merge

+ 0 - 2
apps/app/.gitignore

@@ -3,8 +3,6 @@
 /out/
 
 # test
-test/cypress/screenshots
-test/cypress/videos
 .reg
 
 # dist

+ 0 - 30
apps/app/cypress.config.ts

@@ -1,30 +0,0 @@
-import { defineConfig } from 'cypress';
-
-export default defineConfig({
-  e2e: {
-    baseUrl: 'http://localhost:3000',
-    specPattern: 'test/cypress/e2e/**/*.cy.{ts,tsx}',
-    supportFile: 'test/cypress/support/index.ts',
-    setupNodeEvents: (on) => {
-      // change screen size
-      // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
-      on('before:browser:launch', (browser, launchOptions) => {
-        if (browser.name === 'chromium' && browser.isHeadless) {
-          launchOptions.args.push('--window-size=1400,1024');
-          launchOptions.args.push('--force-device-scale-factor=1');
-        }
-        return launchOptions;
-      });
-    },
-    defaultCommandTimeout: 7000,
-  },
-  fileServerFolder: 'test/cypress',
-  fixturesFolder: 'test/cypress/fixtures',
-  screenshotsFolder: 'test/cypress/screenshots',
-  videosFolder: 'test/cypress/videos',
-  video: false,
-
-  viewportWidth: 1400,
-  viewportHeight: 1024,
-
-});

+ 2 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.17",
+  "version": "7.0.18-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -25,7 +25,6 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "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",
-    "cy:run": "cypress run --browser chromium",
     "//// for CI": "",
     "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tspc",
@@ -204,7 +203,7 @@
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
-    "unzip-stream": "^0.3.1",
+    "unzip-stream": "^0.3.2",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
@@ -243,11 +242,9 @@
     "babel-loader": "^8.2.5",
     "bootstrap": "=5.3.2",
     "connect-browser-sync": "^2.1.0",
-    "cypress-real-events": "^1.12.0",
     "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
-    "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",

+ 12 - 11
apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts

@@ -32,14 +32,15 @@ test('Access to /trash page', async({ page }) => {
   await expect(page.getByTestId('trash-page-list')).toBeVisible();
 });
 
-// TODO: Improve collapseSidebar (https://redmine.weseek.co.jp/issues/148538)
-// test('Access to /tags page', async({ page }) => {
-//   await page.goto('/tags');
-
-//   await collapseSidebar(page, false);
-//   await page.getByTestId('grw-sidebar-nav-primary-tags').click();
-//   await expect(page.getByTestId('grw-sidebar-content-tags')).toBeVisible();
-//   await expect(page.getByTestId('grw-tags-list').first()).toBeVisible();
-//   await expect(page.getByTestId('grw-tags-list').first()).toContainText('You have no tag, You can set tags on pages');
-//   await expect(page.getByTestId('tags-page')).toBeVisible();
-// });
+test('Access to /tags page', async({ page }) => {
+  await page.goto('/');
+
+  await collapseSidebar(page, false);
+  await page.getByTestId('grw-sidebar-nav-primary-tags').click();
+  await expect(page.getByTestId('grw-sidebar-content-tags')).toBeVisible();
+  await expect(page.getByTestId('grw-tags-list').first()).toBeVisible();
+  await expect(page.getByTestId('grw-tags-list').first()).toContainText('You have no tag, You can set tags on pages');
+
+  await page.getByTestId('check-all-tags-button').click();
+  await expect(page.getByTestId('tags-page')).toBeVisible();
+});

+ 170 - 0
apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts

@@ -0,0 +1,170 @@
+import { test, expect } from '@playwright/test';
+
+import { collapseSidebar } from '../utils';
+
+
+test.describe('Access to sidebar', () => {
+
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+    await collapseSidebar(page, false);
+  });
+
+  test('Successfully show sidebar', async({ page }) => {
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+  });
+
+  test('Successfully access to page tree', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-page-tree').click();
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+    await expect(page.getByTestId('grw-pagetree-item-container').first()).toBeVisible();
+  });
+
+  test('Successfully access to recent changes', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-recent-changes').click();
+    await expect(page.getByTestId('grw-recent-changes')).toBeVisible();
+    await expect(page.locator('.list-group-item').first()).toBeVisible();
+  });
+
+  test('Successfully access to custom sidebar', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-custom-sidebar').click();
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+    await expect(page.locator('.grw-sidebar-content-header > h3').locator('a')).toBeVisible();
+  });
+
+  test('Successfully access to GROWI Docs page', async({ page }) => {
+    const linkElement = page.locator('.grw-sidebar-nav-secondary-container a[href*="https://docs.growi.org"]');
+    const docsUrl = await linkElement.getAttribute('href');
+    if (docsUrl == null) {
+      throw new Error('url is null');
+    }
+    const response = await page.request.get(docsUrl);
+    const body = await response.text();
+    expect(body).toContain('</html>');
+  });
+
+  test('Successfully access to trash page', async({ page }) => {
+    await page.locator('.grw-sidebar-nav-secondary-container a[href*="/trash"]').click();
+    await expect(page.getByTestId('trash-page-list')).toBeVisible();
+  });
+
+
+  //
+  // Deactivate: An error occurs that cannot be reproduced in the development environment. -- Yuki Takei 2024.05.10
+  //
+
+  // it('Successfully click Add to Bookmarks button', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     // take a screenshot for dropdown menu
+  //     cy.screenshot(`${ssPrefix}page-tree-2-before-adding-bookmark`)
+  //     // click add remove bookmark btn
+  //     cy.getByTestid('add-bookmark-btn').click();
+  //   })
+
+  //   // show dropdown again
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     // expect to be visible
+  //     cy.getByTestid('remove-bookmark-btn').should('be.visible');
+  //     // take a screenshot for dropdown menu
+  //     cy.screenshot(`${ssPrefix}page-tree-2-after-adding-bookmark`);
+  //   });
+  // });
+
+  // it('Successfully show duplicate page modal', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('open-page-duplicate-modal-btn').click();
+  //   })
+
+  //   cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
+  //     cy.get('.form-control').type('_test');
+
+  //     cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`, { blackout: blackoutOverride });
+
+  //     cy.get('.modal-header > button').click();
+  //   });
+  // });
+
+  // it('Successfully rename page', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('rename-page-btn').click();
+  //   })
+
+  //   cy.getByTestid('grw-sidebar-contents').within(() => {
+  //     cy.getByTestid('autosize-submittable-input').type('_newname');
+  //   })
+
+  //   cy.screenshot(`${ssPrefix}page-tree-6-rename-page`, { blackout: blackoutOverride });
+  // });
+
+  // it('Successfully show delete page modal', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('open-page-delete-modal-btn').click();
+  //   })
+
+  //   cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+  //     cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`, { blackout: blackoutOverride });
+  //     cy.get('.modal-header > button').click();
+  //   });
+  // });
+
+});

+ 43 - 0
apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts

@@ -0,0 +1,43 @@
+import { test } from '@playwright/test';
+
+import { collapseSidebar } from '../utils';
+
+
+test('Switch sidebar mode', async({ page }) => {
+  await page.goto('/');
+  await collapseSidebar(page, false);
+  await collapseSidebar(page, true);
+});
+
+// Write tests using VRT
+// context('Switch viewport size', () => {
+//   const ssPrefix = 'switch-viewport-size-';
+
+//   const sizes = {
+//     'xl': [1200, 1024],
+//     'lg': [992, 1024],
+//     'md': [768, 1024],
+//     'sm': [576, 1024],
+//     'xs': [575, 1024],
+//     'iphone-x': [375, 812],
+//   };
+
+//   Object.entries(sizes).forEach(([screenLabel, size]) => {
+//     it(`on ${screenLabel} screen`, () => {
+//       cy.viewport(size[0], size[1]);
+
+//       // login
+//       cy.fixture("user-admin.json").then(user => {
+//         cy.login(user.username, user.password);
+//       });
+//       cy.visit('/');
+
+//       cy.get('.layout-root').should('be.visible');
+
+//       cy.screenshot(`${ssPrefix}-${screenLabel}`, {
+//         blackout: blackoutOverride,
+//       });
+//     });
+//   });
+
+// });

+ 0 - 2
apps/app/playwright/utils/CollapseSidebar.ts

@@ -1,4 +1,3 @@
-// TODO: https://redmine.weseek.co.jp/issues/148538
 import { expect, type Page } from '@playwright/test';
 
 export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<void> => {
@@ -9,7 +8,6 @@ export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<
 
   const collapseSidebarToggle = page.getByTestId('btn-toggle-collapse');
   await expect(collapseSidebarToggle).toBeVisible();
-
   await collapseSidebarToggle.click();
 
   if (isCollapsed) {

+ 1 - 1
apps/app/regconfig.json

@@ -1,7 +1,7 @@
 {
   "core": {
     "workingDir": ".reg",
-    "actualDir": "test/cypress/screenshots",
+    "actualDir": "test/playwright/screenshots",
     "thresholdRate": 0.001,
     "addIgnore": true,
     "ximgdiff": {

+ 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>
   );
 };

+ 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 - 1
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -211,7 +211,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     finally {
       mutateWaitingSaveProcessing(false);
     }
-  }, [pageId, selectedGrant, mutateWaitingSaveProcessing, t, mutateIsGrantNormalized]);
+  }, [pageId, selectedGrant, mutateWaitingSaveProcessing, updatePage, mutateIsGrantNormalized, t]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: SaveOptions) => {
     const markdown = codeMirrorEditor?.getDoc();

+ 16 - 11
apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx

@@ -10,6 +10,7 @@ import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter, Button,
 } from 'reactstrap';
+import SimpleBar from 'simplebar-react';
 
 import type { IPageForItem } from '~/interfaces/page';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
@@ -22,6 +23,7 @@ import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 
 import { TreeItemForModal } from './TreeItemForModal';
 
+
 export const PageSelectModal: FC = () => {
   const {
     data: PageSelectModalData,
@@ -82,20 +84,23 @@ export const PageSelectModal: FC = () => {
       isOpen={isOpened}
       toggle={closeModal}
       centered
-      size="sm"
     >
       <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
-      <ModalBody>
+      <ModalBody className="p-0">
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
-          <ItemsTree
-            CustomTreeItem={TreeItemForModal}
-            isEnableActions={!isGuestUser}
-            isReadOnlyUser={!!isReadOnlyUser}
-            targetPath={targetPath}
-            targetPathOrId={targetPathOrId}
-            targetAndAncestorsData={targetAndAncestorsData}
-            onClickTreeItem={onClickTreeItem}
-          />
+          <SimpleBar style={{ maxHeight: 'calc(85vh - 133px)' }}> {/* 133px = 63px(ModalHeader) + 70px(ModalFooter) */}
+            <div className="p-3">
+              <ItemsTree
+                CustomTreeItem={TreeItemForModal}
+                isEnableActions={!isGuestUser}
+                isReadOnlyUser={!!isReadOnlyUser}
+                targetPath={targetPath}
+                targetPathOrId={targetPathOrId}
+                targetAndAncestorsData={targetAndAncestorsData}
+                onClickTreeItem={onClickTreeItem}
+              />
+            </div>
+          </SimpleBar>
         </Suspense>
       </ModalBody>
       <ModalFooter>

+ 0 - 36
apps/app/src/client/components/SearchForm.module.scss

@@ -1,36 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-search-table {
-  caption {
-    display: table-header-group;
-  }
-}
-
-@include bs.media-breakpoint-down(sm) {
-  .grw-search-table {
-    th {
-      text-align: right;
-    }
-
-    td {
-      overflow-wrap: anywhere;
-      white-space: normal !important;
-    }
-
-    @include bs.media-breakpoint-down(xs) {
-      th,
-      td {
-        display: block;
-      }
-
-      th {
-        text-align: left;
-      }
-
-      td {
-        padding-top: 0 !important;
-        border-top: none !important;
-      }
-    }
-  }
-}

+ 0 - 143
apps/app/src/client/components/SearchForm.tsx

@@ -1,143 +0,0 @@
-import React, {
-  FC, forwardRef, ForwardRefRenderFunction, useImperativeHandle,
-  useRef, useState,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { IFocusable } from '~/client/interfaces/focusable';
-import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
-import { IPageWithSearchMeta } from '~/interfaces/search';
-
-import SearchTypeahead from './SearchTypeahead';
-
-import styles from './SearchForm.module.scss';
-
-
-type SearchFormHelpProps = {
-  isReachable: boolean,
-}
-
-const SearchFormHelp: FC<SearchFormHelpProps> = React.memo((props: SearchFormHelpProps) => {
-  const { t } = useTranslation();
-
-  const { isReachable } = props;
-
-  if (!isReachable) {
-    return (
-      <>
-        <h5 className="text-danger">Error occured on Search Service</h5>
-        Try to reconnect from management page.
-      </>
-    );
-  }
-
-  return (
-    <table className={`${styles['grw-search-table']} table grw-search-table search-help m-0`}>
-      <caption className="text-start text-primary p-2">
-        <h5 className="h6"><span className="material-symbols-outlined">search</span>{ t('search_help.title') }</h5>
-      </caption>
-      <tbody>
-        <tr>
-          <th className="py-2">
-            <code>word1</code> <code>word2</code><br></br>
-            <small>({ t('search_help.and.syntax help') })</small>
-          </th>
-          <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2">
-            <code>&quot;This is GROWI&quot;</code><br></br>
-            <small>({ t('search_help.phrase.syntax help') })</small>
-          </th>
-          <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-keyword</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>prefix:/user/</code></th>
-          <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-prefix:/user/</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>tag:wiki</code></th>
-          <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-tag:wiki</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
-        </tr>
-      </tbody>
-    </table>
-  );
-});
-
-SearchFormHelp.displayName = 'SearchFormHelp';
-
-
-type Props = TypeaheadProps & {
-  isSearchServiceReachable: boolean,
-
-  keywordOnInit?: string,
-  disableIncrementalSearch?: boolean,
-  onChange?: (data: IPageWithSearchMeta[]) => void,
-  onSubmit?: (input: string) => void,
-};
-
-
-const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
-  const { t } = useTranslation();
-  const {
-    isSearchServiceReachable,
-    keywordOnInit,
-    disableIncrementalSearch,
-    dropup, onChange, onBlur, onFocus, onSubmit, onInputChange,
-  } = props;
-
-  const [searchError, setSearchError] = useState<Error | null>(null);
-
-  const searchTyheaheadRef = useRef<IFocusable>(null);
-
-  // publish focus()
-  useImperativeHandle(ref, () => ({
-    focus() {
-      const instance = searchTyheaheadRef?.current;
-      if (instance != null) {
-        instance.focus();
-      }
-    },
-  }));
-
-  const placeholder = isSearchServiceReachable
-    ? 'Search ...'
-    : 'Error on Search Service';
-
-  const emptyLabel = (searchError != null)
-    ? 'Error on searching.'
-    : t('search.search page bodies');
-
-  return (
-    <SearchTypeahead
-      ref={searchTyheaheadRef}
-      dropup={dropup}
-      emptyLabel={emptyLabel}
-      placeholder={placeholder}
-      onChange={onChange}
-      onSubmit={onSubmit}
-      onInputChange={onInputChange}
-      onSearchError={err => setSearchError(err)}
-      onBlur={onBlur}
-      onFocus={onFocus}
-      keywordOnInit={keywordOnInit}
-      disableIncrementalSearch={disableIncrementalSearch}
-      helpElement={<SearchFormHelp isReachable={isSearchServiceReachable} />}
-    />
-  );
-};
-
-export default forwardRef(SearchForm);

+ 1 - 1
apps/app/src/client/components/Sidebar/Tag.tsx

@@ -68,7 +68,7 @@ const Tag: FC = () => {
         )
       }
 
-      <div className="d-flex justify-content-center my-5">
+      <div className="d-flex justify-content-center my-5" data-testid="check-all-tags-button">
         <Link
           href="/tags"
           className="btn btn-primary rounded px-4"

+ 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);

+ 23 - 5
apps/app/src/server/models/page.ts

@@ -65,6 +65,18 @@ type PaginatedPages = {
   offset: number
 }
 
+export type FindRecentUpdatedPagesOption = {
+  offset: number,
+  limit: number,
+  includeWipPage: boolean,
+  includeTrashed: boolean,
+  isRegExpEscapedFromPath: boolean,
+  sort: 'updatedAt'
+  desc: number
+  hideRestrictedByOwner: boolean,
+  hideRestrictedByGroup: boolean,
+}
+
 export type CreateMethod = (path: string, body: string, user, options: IOptionsForCreate) => Promise<HydratedDocument<PageDocument>>
 
 export interface PageModel extends Model<PageDocument> {
@@ -79,7 +91,7 @@ export interface PageModel extends Model<PageDocument> {
   countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   findParentByPath(path: string | null): Promise<HydratedDocument<PageDocument> | null>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
-  findRecentUpdatedPages(path: string, user, option, includeEmpty?: boolean): Promise<PaginatedPages>
+  findRecentUpdatedPages(path: string, user, option: FindRecentUpdatedPagesOption, includeEmpty?: boolean): Promise<PaginatedPages>
   generateGrantCondition(
     user, userGroups: ObjectIdLike[] | null, includeAnyoneWithTheLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): { $or: any[] }
@@ -414,13 +426,19 @@ export class PageQueryBuilder {
   }
 
   // add viewer condition to PageQueryBuilder instance
-  async addViewerCondition(user, userGroups = null, includeAnyoneWithTheLink = false): Promise<PageQueryBuilder> {
+  async addViewerCondition(
+      user,
+      userGroups = null,
+      includeAnyoneWithTheLink = false,
+      showPagesRestrictedByOwner = false,
+      showPagesRestrictedByGroup = false,
+  ): Promise<PageQueryBuilder> {
     const relatedUserGroups = (user != null && userGroups == null) ? [
       ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
       ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
     ] : userGroups;
 
-    this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink);
+    this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
     return this;
   }
 
@@ -664,7 +682,7 @@ schema.statics.countByPathAndViewer = async function(path: string | null, user,
 };
 
 schema.statics.findRecentUpdatedPages = async function(
-    path: string, user, options, includeEmpty = false,
+    path: string, user, options: FindRecentUpdatedPagesOption, includeEmpty = false,
 ): Promise<PaginatedPages> {
 
   const sortOpt = {};
@@ -690,7 +708,7 @@ schema.statics.findRecentUpdatedPages = async function(
 
   queryBuilder.addConditionToListWithDescendants(path, options);
   queryBuilder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-  await queryBuilder.addViewerCondition(user);
+  await queryBuilder.addViewerCondition(user, undefined, undefined, !options.hideRestrictedByOwner, !options.hideRestrictedByGroup);
   const pages = await Page.paginate(queryBuilder.query.clone(), {
     lean: true, sort: sortOpt, offset: options.offset, limit: options.limit,
   });

+ 2 - 2
apps/app/src/server/models/user-group-relation.ts

@@ -128,7 +128,7 @@ schema.statics.findAllRelationForUserGroups = function(userGroups) {
  * @memberof UserGroupRelation
  */
 schema.statics.findAllGroupsForUser = async function(user): Promise<UserGroupDocument[]> {
-  const userGroupRelations = await this.find({ relatedUser: user.id }).populate('relatedGroup');
+  const userGroupRelations = await this.find({ relatedUser: user._id }).populate('relatedGroup');
   const userGroups = userGroupRelations.map((relation) => {
     return isPopulated(relation.relatedGroup) ? relation.relatedGroup as UserGroupDocument : null;
   });
@@ -161,7 +161,7 @@ schema.statics.findAllUserGroupIdsRelatedToUser = async function(user): Promise<
 schema.statics.countByGroupIdsAndUser = async function(userGroupIds: ObjectIdLike[], userData): Promise<number> {
   const query = {
     relatedGroup: { $in: userGroupIds },
-    relatedUser: userData.id,
+    relatedUser: userData._id,
   };
 
   return this.count(query);

+ 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

+ 8 - 1
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -9,6 +9,7 @@ import { query, oneOf } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
+import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
 import loggerFactory from '~/utils/logger';
 
@@ -18,6 +19,7 @@ import type { PageDocument, PageModel } from '../../models/page';
 
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page-tree');
 
 /*
@@ -103,8 +105,13 @@ const routerFactory = (crowi: Crowi): Router => {
 
     const pageService = crowi.pageService;
 
+    const hideRestrictedByOwner = await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const hideRestrictedByGroup = await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
+
     try {
-      const pages = await pageService.findChildrenByParentPathOrIdAndViewer((id || path)as string, req.user);
+      const pages = await pageService.findChildrenByParentPathOrIdAndViewer(
+        (id || path)as string, req.user, undefined, !hideRestrictedByOwner, !hideRestrictedByGroup,
+      );
       return res.apiv3({ children: pages });
     }
     catch (err) {

+ 3 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -54,7 +54,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
       .withMessage("'revisionId' must be specified"),
     body('body').exists().isString()
       .withMessage("Empty value is not allowed for 'body'"),
-    body('grant').optional().isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
+    body('grant').optional().not().isString()
+      .isInt({ min: 0, max: 5 })
+      .withMessage('grant must be an integer from 1 to 5'),
     body('userRelatedGrantUserGroupIds').optional().isArray().withMessage('userRelatedGrantUserGroupIds must be an array of group id'),
     body('overwriteScopesOfDescendants').optional().isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),

+ 9 - 0
apps/app/src/server/routes/apiv3/pages/index.js

@@ -226,6 +226,12 @@ module.exports = (crowi) => {
     const offset = parseInt(req.query.offset) || 0;
     const includeWipPage = req.query.includeWipPage === 'true'; // Need validation using express-validator
 
+    const hideRestrictedByOwner = await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const hideRestrictedByGroup = await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
+
+    /**
+    * @type {import('~/server/models/page').FindRecentUpdatedPagesOption}
+    */
     const queryOptions = {
       offset,
       limit,
@@ -234,7 +240,10 @@ module.exports = (crowi) => {
       isRegExpEscapedFromPath: true,
       sort: 'updatedAt',
       desc: -1,
+      hideRestrictedByOwner,
+      hideRestrictedByGroup,
     };
+
     try {
       const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
       if (result.pages.length > limit) {

+ 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';

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

@@ -4332,8 +4332,13 @@ class PageService implements IPageService {
   /*
    * Find all children by parent's path or id. Using id should be prioritized
    */
-  async findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups = null)
-      : Promise<(HydratedDocument<PageDocument> & { processData?: IPageOperationProcessData })[]> {
+  async findChildrenByParentPathOrIdAndViewer(
+      parentPathOrId: string,
+      user,
+      userGroups = null,
+      showPagesRestrictedByOwner = false,
+      showPagesRestrictedByGroup = false,
+  ): Promise<(HydratedDocument<PageDocument> & { processData?: IPageOperationProcessData })[]> {
     const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
     let queryBuilder: PageQueryBuilder;
     if (hasSlash(parentPathOrId)) {
@@ -4346,7 +4351,7 @@ class PageService implements IPageService {
       // Use $eq for user-controlled sources. see: https://codeql.github.com/codeql-query-help/javascript/js-sql-injection/#recommendation
       queryBuilder = new PageQueryBuilder(Page.find({ parent: { $eq: parentId } } as any), true); // TODO: improve type
     }
-    await queryBuilder.addViewerCondition(user, userGroups);
+    await queryBuilder.addViewerCondition(user, userGroups, undefined, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
 
     const pages: HydratedDocument<PageDocument>[] = await queryBuilder
       .addConditionToSortPagesByAscPath()

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

@@ -21,7 +21,9 @@ export interface IPageService {
   getEventEmitter: () => EventEmitter,
   deleteMultipleCompletely: (pages: ObjectIdLike[], user: IUser | undefined) => Promise<void>,
   findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>,
-  findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>,
+  findChildrenByParentPathOrIdAndViewer(
+    parentPathOrId: string, user, userGroups?, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
+  ): Promise<PageDocument[]>,
   shortBodiesMapByPageIds(pageIds?: Types.ObjectId[], user?): Promise<Record<string, string | null>>,
   constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): IPageInfo | Omit<IPageInfoForEntity, 'bookmarkCount'>,
   normalizeAllPublicPages(): Promise<void>,

+ 1 - 1
apps/app/src/server/util/compare-objectId.ts

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 
-import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 
 type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Types.ObjectId;

+ 5 - 0
apps/app/src/stores/page.tsx

@@ -81,6 +81,11 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
       return true;
     }
 
+    // mutate When a different revision is opened
+    if (cachedData.revision?._id != null && initialData.revision?._id != null && cachedData.revision._id !== initialData.revision._id) {
+      return true;
+    }
+
     return false;
   })();
 

+ 18 - 4
apps/app/src/stores/ui.tsx

@@ -337,11 +337,25 @@ export const useCommentEditorDirtyMap = (): SWRResponse<Map<string, boolean>, Er
  *********************************************************** */
 
 export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean, Error> => {
-  const { data: currentUser } = useCurrentUser();
-  const { data: isReadOnlyUser } = useIsReadOnlyUser();
-  const { data: isTrashPage } = useIsTrashPage();
+  const key = 'isAbleToShowTrashPageManagementButtons';
 
-  return useStaticSWR('isAbleToShowTrashPageManagementButtons', isTrashPage && currentUser != null && !isReadOnlyUser);
+  const { data: _currentUser } = useCurrentUser();
+  const isCurrentUserExist = _currentUser != null;
+
+  const { data: _currentPageId } = useCurrentPageId();
+  const { data: _isNotFound } = useIsNotFound();
+  const { data: _isTrashPage } = useIsTrashPage();
+  const { data: _isReadOnlyUser } = useIsReadOnlyUser();
+  const isPageExist = _currentPageId != null && _isNotFound === false;
+  const isTrashPage = isPageExist && _isTrashPage === true;
+  const isReadOnlyUser = isPageExist && _isReadOnlyUser === true;
+
+  const includesUndefined = [_currentUser, _currentPageId, _isNotFound, _isReadOnlyUser, _isTrashPage].some(v => v === undefined);
+
+  return useSWRImmutable(
+    includesUndefined ? null : [key, isTrashPage, isCurrentUserExist, isReadOnlyUser],
+    ([, isTrashPage, isCurrentUserExist, isReadOnlyUser]) => isTrashPage && isCurrentUserExist && !isReadOnlyUser,
+  );
 };
 
 export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {

+ 0 - 8
apps/app/test/cypress/.eslintrc.js

@@ -1,8 +0,0 @@
-module.exports = {
-  root: true,
-  extends: [
-    'weseek/typescript',
-    'plugin:cypress/recommended',
-  ],
-  plugins: ['@typescript-eslint', 'cypress'],
-};

+ 0 - 102
apps/app/test/cypress/e2e/0-advanced-examples/misc.cy.ts

@@ -1,102 +0,0 @@
-context('Misc', () => {
-  beforeEach(() => {
-    cy.visit('https://example.cypress.io/commands/misc')
-  })
-
-  it('.end() - end the command chain', () => {
-    // https://on.cypress.io/end
-
-    // cy.end is useful when you want to end a chain of commands
-    // and force Cypress to re-query from the root element
-    cy.get('.misc-table').within(() => {
-      // ends the current chain and yields null
-      cy.contains('Cheryl').click().end()
-
-      // queries the entire table again
-      cy.contains('Charles').click()
-    })
-  })
-
-  it('cy.exec() - execute a system command', () => {
-    // execute a system command.
-    // so you can take actions necessary for
-    // your test outside the scope of Cypress.
-    // https://on.cypress.io/exec
-
-    // we can use Cypress.platform string to
-    // select appropriate command
-    // https://on.cypress/io/platform
-    cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
-
-    // on CircleCI Windows build machines we have a failure to run bash shell
-    // https://github.com/cypress-io/cypress/issues/5169
-    // so skip some of the tests by passing flag "--env circle=true"
-    const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
-
-    if (isCircleOnWindows) {
-      cy.log('Skipping test on CircleCI')
-
-      return
-    }
-
-    // cy.exec problem on Shippable CI
-    // https://github.com/cypress-io/cypress/issues/6718
-    const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
-
-    if (isShippable) {
-      cy.log('Skipping test on ShippableCI')
-
-      return
-    }
-
-    cy.exec('echo Jane Lane')
-      .its('stdout').should('contain', 'Jane Lane')
-
-    if (Cypress.platform === 'win32') {
-      cy.exec('print cypress.json')
-        .its('stderr').should('be.empty')
-    } else {
-      cy.exec('cat cypress.json')
-        .its('stderr').should('be.empty')
-
-      cy.exec('pwd')
-        .its('code').should('eq', 0)
-    }
-  })
-
-  it('cy.focused() - get the DOM element that has focus', () => {
-    // https://on.cypress.io/focused
-    cy.get('.misc-form').find('#name').click()
-    cy.focused().should('have.id', 'name')
-
-    cy.get('.misc-form').find('#description').click()
-    cy.focused().should('have.id', 'description')
-  })
-
-  context('Cypress.Screenshot', function () {
-    it('cy.screenshot() - take a screenshot', () => {
-      // https://on.cypress.io/screenshot
-      cy.screenshot('my-image')
-    })
-
-    it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
-      Cypress.Screenshot.defaults({
-        blackout: ['.foo'],
-        capture: 'viewport',
-        clip: { x: 0, y: 0, width: 200, height: 200 },
-        scale: false,
-        disableTimersAndAnimations: true,
-        screenshotOnRunFailure: true,
-        onBeforeScreenshot () { },
-        onAfterScreenshot () { },
-      })
-    })
-  })
-
-  it('cy.wrap() - wrap an object', () => {
-    // https://on.cypress.io/wrap
-    cy.wrap({ foo: 'bar' })
-      .should('have.property', 'foo')
-      .and('include', 'bar')
-  })
-})

+ 0 - 59
apps/app/test/cypress/e2e/0-advanced-examples/viewport.cy.ts

@@ -1,59 +0,0 @@
-context('Viewport', () => {
-  beforeEach(() => {
-    cy.visit('https://example.cypress.io/commands/viewport')
-  })
-
-  it('cy.viewport() - set the viewport size and dimension', () => {
-    // https://on.cypress.io/viewport
-
-    cy.get('#navbar').should('be.visible')
-    cy.viewport(320, 480)
-
-    // the navbar should have collapse since our screen is smaller
-    cy.get('#navbar').should('not.be.visible')
-    cy.get('.navbar-toggle').should('be.visible').click()
-    cy.get('.nav').find('a').should('be.visible')
-
-    // lets see what our app looks like on a super large screen
-    cy.viewport(2999, 2999)
-
-    // cy.viewport() accepts a set of preset sizes
-    // to easily set the screen to a device's width and height
-
-    // We added a cy.wait() between each viewport change so you can see
-    // the change otherwise it is a little too fast to see :)
-
-    /* eslint-disable cypress/no-unnecessary-waiting */
-    cy.viewport('macbook-15')
-    cy.wait(200)
-    cy.viewport('macbook-13')
-    cy.wait(200)
-    cy.viewport('macbook-11')
-    cy.wait(200)
-    cy.viewport('ipad-2')
-    cy.wait(200)
-    cy.viewport('ipad-mini')
-    cy.wait(200)
-    cy.viewport('iphone-6+')
-    cy.wait(200)
-    cy.viewport('iphone-6')
-    cy.wait(200)
-    cy.viewport('iphone-5')
-    cy.wait(200)
-    cy.viewport('iphone-4')
-    cy.wait(200)
-    cy.viewport('iphone-3')
-    cy.wait(200)
-
-    // cy.viewport() accepts an orientation for all presets
-    // the default orientation is 'portrait'
-    cy.viewport('ipad-2', 'portrait')
-    cy.wait(200)
-    cy.viewport('iphone-4', 'landscape')
-    cy.wait(200)
-    /* eslint-enable cypress/no-unnecessary-waiting */
-
-    // The viewport will be reset back to the default dimensions
-    // in between tests (the  default can be set in cypress.json)
-  })
-})

+ 0 - 18
apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts

@@ -1,18 +0,0 @@
-context('Access to special pages by guest', () => {
-  const ssPrefix = 'access-to-special-pages-by-guest-';
-  it('/tags is successfully loaded', () => {
-    cy.visit('/tags');
-
-    // open sidebar
-    cy.collapseSidebar(false);
-    // select tags
-    cy.getByTestid('grw-sidebar-nav-primary-tags').click();
-    cy.getByTestid('grw-sidebar-content-tags').should('be.visible');
-    cy.getByTestid('grw-tags-list').should('be.visible');
-    cy.getByTestid('grw-tags-list').contains('You have no tag, You can set tags on pages');
-
-    cy.getByTestid('tags-page').should('be.visible');
-    cy.screenshot(`${ssPrefix}-tags`);
-  });
-
-});

+ 0 - 303
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -1,303 +0,0 @@
-import { BlackoutGroup } from "../../support/blackout";
-
-// Blackout for recalculation of toc content hight
-const blackoutOverride = [
-  ...BlackoutGroup.BASIS,
-  ...BlackoutGroup.SIDE_CONTENTS,
-];
-
-describe('Access to sidebar', () => {
-  const ssPrefix = 'access-to-sidebar-';
-
-  context('when logged in', () => {
-    beforeEach(() => {
-      // login
-      cy.fixture("user-admin.json").then(user => {
-        cy.login(user.username, user.password);
-      });
-    });
-
-    context('when access to root page', { scrollBehavior: false }, () => {
-      beforeEach(() => {
-        cy.visit('/');
-
-        // Since this is a sidebar test, call collapseSidebar in beforeEach.
-        cy.collapseSidebar(false, true);
-      });
-
-      describe('Test show/collapse button', () => {
-        it('Successfully show sidebar', () => {
-          cy.getByTestid('grw-sidebar-contents').should('be.visible');
-
-          cy.waitUntilSkeletonDisappear();
-          cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
-            capture: 'viewport',
-            blackout: blackoutOverride,
-          });
-        });
-
-        it('Successfully collapse sidebar', () => {
-          cy.getByTestid('btn-toggle-collapse').click({force: true});
-
-          cy.getByTestid('grw-sidebar-contents').should('not.be.visible');
-
-          cy.waitUntilSkeletonDisappear();
-          cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
-            capture: 'viewport',
-            blackout: blackoutOverride,
-          });
-        });
-      });
-
-      describe('Test page tree tab', () => {
-        beforeEach(() => {
-          cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
-        });
-
-        it('Successfully access to page tree', () => {
-          cy.getByTestid('grw-sidebar-contents').within(() => {
-            cy.getByTestid('grw-pagetree-item-container').should('be.visible');
-
-            cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`, { blackout: blackoutOverride });
-          });
-        });
-
-
-        //
-        // Deactivate: An error occurs that cannot be reproduced in the development environment. -- Yuki Takei 2024.05.10
-        //
-
-        // it('Successfully click Add to Bookmarks button', () => {
-        //   cy.waitUntil(() => {
-        //     // do
-        //     cy.getByTestid('grw-sidebar-contents').within(() => {
-        //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
-        //         cy.get('li').realHover();
-        //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
-        //       });
-        //     });
-        //     // wait until
-        //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
-        //   });
-
-        //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-        //     // take a screenshot for dropdown menu
-        //     cy.screenshot(`${ssPrefix}page-tree-2-before-adding-bookmark`)
-        //     // click add remove bookmark btn
-        //     cy.getByTestid('add-bookmark-btn').click();
-        //   })
-
-        //   // show dropdown again
-        //   cy.waitUntil(() => {
-        //     // do
-        //     cy.getByTestid('grw-sidebar-contents').within(() => {
-        //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
-        //         cy.get('li').realHover();
-        //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
-        //       });
-        //     });
-        //     // wait until
-        //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
-        //   });
-
-        //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-        //     // expect to be visible
-        //     cy.getByTestid('remove-bookmark-btn').should('be.visible');
-        //     // take a screenshot for dropdown menu
-        //     cy.screenshot(`${ssPrefix}page-tree-2-after-adding-bookmark`);
-        //   });
-        // });
-
-        // it('Successfully show duplicate page modal', () => {
-        //   cy.waitUntil(() => {
-        //     // do
-        //     cy.getByTestid('grw-sidebar-contents').within(() => {
-        //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
-        //         cy.get('li').realHover();
-        //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
-        //       });
-        //     });
-        //     // wait until
-        //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
-        //   });
-
-        //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-        //     cy.getByTestid('open-page-duplicate-modal-btn').click();
-        //   })
-
-        //   cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
-        //     cy.get('.form-control').type('_test');
-
-        //     cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`, { blackout: blackoutOverride });
-
-        //     cy.get('.modal-header > button').click();
-        //   });
-        // });
-
-        // it('Successfully rename page', () => {
-        //   cy.waitUntil(() => {
-        //     // do
-        //     cy.getByTestid('grw-sidebar-contents').within(() => {
-        //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
-        //         cy.get('li').realHover();
-        //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
-        //       });
-        //     });
-        //     // wait until
-        //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
-        //   });
-
-        //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-        //     cy.getByTestid('rename-page-btn').click();
-        //   })
-
-        //   cy.getByTestid('grw-sidebar-contents').within(() => {
-        //     cy.getByTestid('autosize-submittable-input').type('_newname');
-        //   })
-
-        //   cy.screenshot(`${ssPrefix}page-tree-6-rename-page`, { blackout: blackoutOverride });
-        // });
-
-        // it('Successfully show delete page modal', () => {
-        //   cy.waitUntil(() => {
-        //     // do
-        //     cy.getByTestid('grw-sidebar-contents').within(() => {
-        //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
-        //         cy.get('li').realHover();
-        //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
-        //       });
-        //     });
-        //     // wait until
-        //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
-        //   });
-
-        //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-        //     cy.getByTestid('open-page-delete-modal-btn').click();
-        //   })
-
-        //   cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-        //     cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`, { blackout: blackoutOverride });
-        //     cy.get('.modal-header > button').click();
-        //   });
-        // });
-      });
-
-      describe('Test custom sidebar tab', () => {
-        beforeEach(() => {
-          cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar').click();
-        });
-
-        it('Successfully access to custom sidebar', () => {
-          cy.getByTestid('grw-sidebar-contents').within(() => {
-            cy.get('.grw-sidebar-content-header > h4').find('a');
-
-            cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`, { blackout: blackoutOverride });
-          });
-        });
-
-        // TODO: fix by https://redmine.weseek.co.jp/issues/138562
-        // it('Successfully redirect to editor', () => {
-        //   const content = '# HELLO \n ## Hello\n ### Hello';
-
-        //   cy.get('.grw-sidebar-content-header > h3 > a').should('be.visible').click();
-
-        //   cy.get('.layout-root').should('have.class', 'editing');
-        //   cy.get('.CodeMirror textarea').type(content, {force: true});
-
-        //   cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
-
-        //   cy.getByTestid('save-page-btn').click();
-        // });
-
-        // it('Successfully create custom sidebar content', () => {
-        //   cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar')
-        //     .should('be.visible')
-        //     .should('have.class', 'active');
-
-        //   cy.waitUntilSkeletonDisappear();
-        //   cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`, { blackout: blackoutOverride });
-        // });
-      });
-
-      describe('Test recent changes tab', () => {
-        beforeEach(() => {
-          cy.getByTestid('grw-sidebar-nav-primary-recent-changes').click();
-        });
-
-        it('Successfully access to recent changes', () => {
-          cy.getByTestid('grw-recent-changes').should('be.visible');
-          cy.get('.list-group-item').should('be.visible');
-
-          // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-access-to-recent-changes`, { blackout: blackoutOverride });
-        });
-
-      });
-
-      //
-      // Deactivate: An error occurs that cannot be reproduced in the development environment. -- Yuki Takei 2024.05.10
-      //
-      // describe('Test tags tab', () => {
-      //   beforeEach(() => {
-      //     cy.getByTestid('grw-sidebar-nav-primary-tags').click();
-      //   });
-
-      //   it('Successfully access to tags', () => {
-      //     cy.getByTestid('grw-sidebar-contents').within(() => {
-      //       cy.getByTestid('grw-tags-list').should('be.visible');
-
-      //       cy.screenshot(`${ssPrefix}tags-1-access-to-tags`, { blackout: blackoutOverride });
-      //     });
-      //   });
-
-      //   it('Succesfully click all tags button', () => {
-      //     cy.getByTestid('grw-sidebar-content-tags').within(() => {
-      //       cy.get('.btn-primary').as('check-all-tags-button');
-      //       cy.get('@check-all-tags-button').should('be.visible');
-      //       cy.get('@check-all-tags-button').click({force: true});
-      //     });
-      //     cy.collapseSidebar(true);
-      //     cy.getByTestid('grw-tags-list').should('be.visible');
-
-      //     cy.screenshot(`${ssPrefix}tags-2-click-all-tags-button`, { blackout: blackoutOverride });
-      //   });
-      // });
-
-      // // TODO: No Drafts pages on GROWI version 6
-      // it('Successfully access to My Drafts page', () => {
-      //   cy.visit('/');
-      //   cy.collapseSidebar(true);
-      //   cy.get('.grw-sidebar-nav-secondary-container').within(() => {
-      //     cy.get('a[href*="/me/drafts"]').click();
-      //   });
-      //   cy.screenshot(`${ssPrefix}access-to-drafts-page`, { blackout: blackoutOverride });
-      // });
-
-      describe('Test access to GROWI Docs page', () => {
-        it('Successfully access to GROWI Docs page', () => {
-          cy.get('.grw-sidebar-nav-secondary-container').within(() => {
-            cy.get('a[href*="https://docs.growi.org"]').then(($a) => {
-              const url = $a.prop('href')
-              cy.request(url).its('body').should('include', '</html>');
-            });
-          });
-        });
-      });
-
-      describe('Test access to trash page', () => {
-        it('Successfully access to trash page', () => {
-          cy.collapseSidebar(true);
-          cy.get('.grw-sidebar-nav-secondary-container').within(() => {
-            cy.get('a[href*="/trash"]').click();
-          });
-
-          cy.getByTestid('trash-page-list').should('be.visible');
-
-          cy.screenshot(`${ssPrefix}access-to-trash-page`, { blackout: blackoutOverride });
-        });
-      });
-    });
-  });
-});

+ 0 - 65
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts

@@ -1,65 +0,0 @@
-import { BlackoutGroup } from "../../support/blackout";
-
-// Blackout for recalculation of toc content hight
-const blackoutOverride = [
-  ...BlackoutGroup.BASIS,
-  ...BlackoutGroup.SIDE_CONTENTS,
-];
-
-context('Switch sidebar mode', () => {
-  const ssPrefix = 'switch-sidebar-mode-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-    cy.visit('/');
-  });
-
-  it('Switching sidebar mode', () => {
-    cy.collapseSidebar(false);
-    cy.screenshot(`${ssPrefix}-doc-mode-opened`, {
-      blackout: blackoutOverride,
-    });
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-doc-mode-closed`, {
-      blackout: blackoutOverride,
-    });
-  });
-
-});
-
-context('Switch viewport size', () => {
-  const ssPrefix = 'switch-viewport-size-';
-
-  const sizes = {
-    'xl': [1200, 1024],
-    'lg': [992, 1024],
-    'md': [768, 1024],
-    'sm': [576, 1024],
-    'xs': [575, 1024],
-    'iphone-x': [375, 812],
-  };
-
-  Object.entries(sizes).forEach(([screenLabel, size]) => {
-    it(`on ${screenLabel} screen`, () => {
-      cy.viewport(size[0], size[1]);
-
-      // login
-      cy.fixture("user-admin.json").then(user => {
-        cy.login(user.username, user.password);
-      });
-      cy.visit('/');
-
-      cy.get('.layout-root').should('be.visible');
-
-      cy.screenshot(`${ssPrefix}-${screenLabel}`, {
-        blackout: blackoutOverride,
-      });
-    });
-  });
-
-});
-

+ 0 - 6
apps/app/test/cypress/fixtures/user-admin.json

@@ -1,6 +0,0 @@
-{
-  "username": "admin",
-  "name": "Admin",
-  "email": "admin@example.com",
-  "password": "adminadmin"
-}

+ 0 - 21
apps/app/test/cypress/support/assertions.ts

@@ -1,21 +0,0 @@
-// from https://github.com/cypress-io/cypress/issues/877#issuecomment-538708750
-const isInViewport = (_chai) => {
-  function assertIsInViewport() {
-
-    const subject = this._obj;
-
-    const bottom = Cypress.config("viewportWidth");
-    const rect = subject[0].getBoundingClientRect();
-
-    this.assert(
-      rect.top < bottom && rect.bottom < bottom,
-      "expected #{this} to be in viewport",
-      "expected #{this} to not be in viewport",
-      this._obj
-    )
-  }
-
-  _chai.Assertion.addMethod('inViewport', assertIsInViewport)
-};
-
-chai.use(isInViewport);

+ 0 - 14
apps/app/test/cypress/support/blackout.ts

@@ -1,14 +0,0 @@
-export const BlackoutGroup = {
-  BASIS: [
-    '[data-vrt-blackout=true]',
-    '[data-vrt-blackout-hash=true]',
-    '[data-vrt-blackout-profile=true]',
-    '[data-vrt-blackout-datetime=true]',
-  ],
-  SIDEBAR_NAV: [
-    '[data-vrt-blackout-sidebar-nav=true]',
-  ],
-  SIDE_CONTENTS: [
-    '[data-vrt-blackout-side-contents=true]',
-  ],
-} as const;

+ 0 - 118
apps/app/test/cypress/support/commands.ts

@@ -1,118 +0,0 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add('login', (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-
-import 'cypress-wait-until';
-
-function isVisible($elem: JQuery<Element>) {
-  return $elem.is(':visible');
-}
-function isHidden($elem: JQuery<Element>) {
-  return !isVisible($elem);
-}
-function isVisibleByTestId(testId: string) {
-  return isVisible(Cypress.$(`[data-testid=${testId}]`));
-}
-function isHiddenByTestId(testId: string) {
-  return !isVisibleByTestId(testId);
-}
-
-Cypress.Commands.add('getByTestid', (selector, options?) => {
-  return cy.get(`[data-testid=${selector}]`, options);
-});
-
-Cypress.Commands.add('login', (username, password) => {
-  cy.session(username, () => {
-    cy.visit('/page-to-return-after-login');
-    cy.getByTestid('tiUsernameForLogin').type(username);
-    cy.getByTestid('tiPasswordForLogin').type(password);
-
-    cy.intercept('POST', '/_api/v3/login').as('login');
-    cy.getByTestid('btnSubmitForLogin').click();
-    cy.wait('@login')
-  });
-});
-
-Cypress.Commands.add('waitUntilSkeletonDisappear', () => {
-  if (isHidden(Cypress.$('.grw-skeleton'))) {
-    return;
-  }
-  cy.get('.grw-skeleton').should('not.exist');
-});
-
-Cypress.Commands.add('waitUntilSpinnerDisappear', () => {
-  if (isHidden(Cypress.$('.fa-spinner'))) {
-    return;
-  }
-  cy.get('.fa-spinner').should('not.exist');
-});
-
-Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving = false) => {
-  cy.getByTestid('grw-sidebar').within(($sidebar) => {
-
-    // skip if .grw-sidebar-dock does not exist
-    if (!$sidebar.hasClass('grw-sidebar-dock')) {
-      return;
-    }
-
-  });
-
-  cy.getByTestid('grw-sidebar').should('be.visible').within(() => {
-
-    const isSidebarContentsHidden = isHiddenByTestId('grw-sidebar-contents');
-    if (isSidebarContentsHidden === isCollapsed) {
-      return;
-    }
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid("btn-toggle-collapse").click({force: true});
-      // wait until saving UserUISettings
-      if (waitUntilSaving) {
-        // eslint-disable-next-line cypress/no-unnecessary-waiting
-        cy.wait(1500);
-      }
-
-      // wait until
-      return cy.getByTestid('grw-sidebar-contents').then($contents => isHidden($contents) === isCollapsed);
-    });
-  });
-
-});
-
-Cypress.Commands.add('appendTextToEditorUntilContains', (inputText: string) => {
-  const lines: string[] = [];
-  cy.waitUntil(() => {
-    // do
-    cy.get('.CodeMirror textarea').type(inputText, { force: true });
-    // until
-    return cy.get('.CodeMirror-line')
-      .each(($item) => {
-        lines.push($item.text());
-      }).then(() => {
-        return lines.join('\n').endsWith(inputText);
-      });
-  });
-});

+ 0 - 48
apps/app/test/cypress/support/index.ts

@@ -1,48 +0,0 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
-import 'cypress-real-events';
-
-import './assertions'
-import './commands'
-import './screenshot'
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
-
-// Ignore 'ResizeObserver loop limit exceeded' exception
-// https://github.com/cypress-io/cypress/issues/8418
-const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
-Cypress.on('uncaught:exception', (err) => {
-    /* returning false here prevents Cypress from failing the test */
-    if (resizeObserverLoopErrRe.test(err.message)) {
-        return false
-    }
-})
-
-declare global {
-  // eslint-disable-next-line @typescript-eslint/no-namespace
-  namespace Cypress {
-    interface Chainable {
-       getByTestid(selector: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<Element>>,
-       login(username: string, password: string): Chainable<void>,
-       collapseSidebar(isCollapsed: boolean, waitUntilSaving?: boolean): Chainable<void>,
-       waitUntilSkeletonDisappear(): Chainable<void>,
-       waitUntilSpinnerDisappear(): Chainable<void>,
-       appendTextToEditorUntilContains(inputText: string): Chainable<void>
-    }
-  }
-}

+ 0 - 9
apps/app/test/cypress/support/screenshot.ts

@@ -1,9 +0,0 @@
-import { BlackoutGroup } from "./blackout";
-
-Cypress.Screenshot.defaults({
-  blackout: [
-    ...BlackoutGroup.BASIS,
-    ...BlackoutGroup.SIDEBAR_NAV,
-  ],
-  capture: 'viewport',
-})

+ 0 - 16
apps/app/test/cypress/tsconfig.json

@@ -1,16 +0,0 @@
-{
-  "extends": "../../tsconfig.json",
-  "compilerOptions": {
-    "noEmit": true,
-    // be explicit about types included
-    // to avoid clashing with Jest types
-    "types": ["cypress", "cypress-real-events"],
-    // turn off sourceMap
-    // see: https://github.com/cypress-io/cypress/issues/26203
-    "sourceMap": false
-  },
-  "include": [
-    "../../node_modules/cypress",
-    "./**/*.ts"
-  ]
-}

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.0.17-slackbot-proxy.0",
+  "version": "7.0.18-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"]
+    }
+
+  }
+}

+ 0 - 53
bin/github-actions/generate-cypress-spec-arg.mjs

@@ -1,53 +0,0 @@
-/* eslint-disable no-console */
-
-/*
- * USAGE:
- *  node generate-cypress-spec-arg --prefix=${prefix} --suffix=${suffix} ${value}
- *
- * OPTIONS:
- *  --prefix : prefix string for each items
- *  --suffix : suffix string for each items
- *
- * EXAMPLE:
- *  node generate-cypress-spec-arg --prefix=${prefix}"A" --suffix="Z" "1,3,5"
- *  => A1Z,A3Z,A5Z
- */
-
-import yargs from 'yargs';
-import { hideBin } from 'yargs/helpers';
-
-const argv = yargs(hideBin(process.argv)).argv;
-
-
-const printExample = () => {
-  console.group('** Usage **');
-  // eslint-disable-next-line no-template-curly-in-string
-  console.log('$ node generate-cypress-spec-arg --prefix=${prefix}"A" --suffix="Z" "1,3,5"');
-  console.log('  ==> A1Z,A3Z,A5Z');
-  console.groupEnd();
-  console.log('\n');
-};
-
-
-const { prefix, suffix, _: value } = argv;
-
-if (prefix == null) {
-  printExample();
-  throw new Error('The option "prefix" must be specified');
-}
-if (suffix == null) {
-  printExample();
-  throw new Error('The option "suffix" must be specified');
-}
-if (value.length === 0) {
-  printExample();
-  throw new Error('A value string must be specified');
-}
-
-const result = value[0]
-  .toString().split(',')
-  .map(v => v.trim())
-  .map(v => `${prefix}${v}${suffix}`)
-  .join(',');
-
-console.log(result);

+ 1 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.0.17",
+  "version": "7.0.18-RC.0",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "private": "true",
@@ -71,8 +71,6 @@
     "@vitejs/plugin-react": "^4.3.1",
     "@vitest/coverage-v8": "^1.6.0",
     "@vitest/ui": "^1.6.0",
-    "cypress": "^13.3.0",
-    "cypress-wait-until": "^2.0.1",
     "eslint": "^8.41.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.1",

+ 1 - 0
packages/editor/src/client/components/CodeMirrorEditorReadOnly.tsx

@@ -13,6 +13,7 @@ const additionalExtensions: Extension[] = [
   [
     setDataLine,
     EditorState.readOnly.of(true),
+    EditorView.editable.of(false),
   ],
 ];
 

+ 1 - 1
packages/editor/src/client/stores/codemirror-editor.ts

@@ -33,7 +33,7 @@ export const useCodeMirrorEditorIsolated = (
 
   const newData = useCodeMirrorEditor(mergedProps);
 
-  const shouldUpdate = swrKey != null && container != null && props != null && (
+  const shouldUpdate = swrKey != null && container != null && (
     currentData == null
     || (isValid(newData) && !isDeepEquals(currentData, newData))
   );

+ 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>

+ 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"

Разница между файлами не показана из-за своего большого размера
+ 234 - 336
yarn.lock


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