Taichi Masuyama 4 anni fa
parent
commit
915c0cb85d
77 ha cambiato i file con 1587 aggiunte e 1021 eliminazioni
  1. 13 9
      .github/release-drafter.yml
  2. 2 1
      .github/workflows/ci-slackbot-proxy.yml
  3. 2 1
      .github/workflows/ci.yml
  4. 8 4
      .github/workflows/draft-release.yml
  5. 6 2
      .github/workflows/pr-to-master.yml
  6. 10 2
      .github/workflows/release.yml
  7. 518 343
      CHANGELOG.md
  8. 0 55
      bin/bump-versions/step/printHelp.js
  9. 0 0
      bin/github-actions/bump-versions/README.md
  10. 4 2
      bin/github-actions/bump-versions/cli.js
  11. 8 8
      bin/github-actions/bump-versions/flow/bump-versions.js
  12. 6 2
      bin/github-actions/bump-versions/index.js
  13. 54 0
      bin/github-actions/bump-versions/step/printHelp.js
  14. 6 6
      bump-versions.config.js
  15. 2 2
      lerna.json
  16. 2 2
      package.json
  17. 2 2
      packages/app/bin/github-actions/update-readme.sh
  18. 6 6
      packages/app/config/migrate.js
  19. 0 2
      packages/app/docker/Dockerfile
  20. 5 5
      packages/app/docker/README.md
  21. 11 13
      packages/app/package.json
  22. 1 1
      packages/app/src/components/PagePathHierarchicalLink.jsx
  23. 1 1
      packages/app/src/components/Sidebar/CustomSidebar.jsx
  24. 138 30
      packages/app/src/components/Sidebar/RecentChanges.jsx
  25. 33 0
      packages/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js
  26. 0 1
      packages/app/src/server/.node-dev.json
  27. 3 1
      packages/app/src/server/console.js
  28. 4 2
      packages/app/src/server/crowi/index.js
  29. 2 1
      packages/app/src/server/middlewares/admin-required.js
  30. 1 1
      packages/app/src/server/models/page.js
  31. 1 0
      packages/app/src/server/models/slack-app-integration.js
  32. 72 4
      packages/app/src/server/routes/apiv3/pages.js
  33. 15 18
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  34. 8 4
      packages/app/src/server/service/search.js
  35. 21 2
      packages/app/src/server/service/slack-integration.ts
  36. 19 12
      packages/app/src/server/service/socket-io.js
  37. 11 1
      packages/app/src/server/util/mongoose-utils.ts
  38. 49 0
      packages/app/src/styles/_recent-changes.scss
  39. 4 0
      packages/app/src/styles/_sidebar.scss
  40. 8 2
      packages/app/src/styles/_tag.scss
  41. 1 0
      packages/app/src/styles/style-app.scss
  42. 1 1
      packages/app/src/styles/theme/_apply-colors-light.scss
  43. 54 0
      packages/app/src/styles/theme/_apply-colors.scss
  44. 4 0
      packages/app/src/test/config/migrate.test.js
  45. 3 1
      packages/app/src/test/global-setup.js
  46. 2 1
      packages/app/src/test/setup.js
  47. 5 4
      packages/core/package.json
  48. 0 1
      packages/plugin-attachment-refs/index.js
  49. 6 5
      packages/plugin-attachment-refs/package.json
  50. 0 0
      packages/plugin-attachment-refs/src/index.js
  51. 25 10
      packages/plugin-attachment-refs/src/server/routes/refs.js
  52. 5 4
      packages/plugin-lsx/package.json
  53. 2 0
      packages/plugin-lsx/src/client-entry.js
  54. 7 2
      packages/plugin-lsx/src/client/css/index.css
  55. 76 46
      packages/plugin-lsx/src/client/js/components/Lsx.jsx
  56. 10 6
      packages/plugin-lsx/src/client/js/components/LsxPageList/LsxListView.jsx
  57. 3 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx
  58. 1 0
      packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx
  59. 33 0
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js
  60. 12 17
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js
  61. 33 62
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js
  62. 0 87
      packages/plugin-lsx/src/client/js/util/LsxCacheHelper.js
  63. 23 83
      packages/plugin-lsx/src/client/js/util/LsxContext.js
  64. 39 0
      packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js
  65. 11 1
      packages/plugin-lsx/src/index.js
  66. 0 11
      packages/plugin-lsx/src/meta.js
  67. 10 8
      packages/plugin-lsx/src/server/routes/index.js
  68. 81 15
      packages/plugin-lsx/src/server/routes/lsx.js
  69. 4 2
      packages/plugin-pukiwiki-like-linker/package.json
  70. 8 1
      packages/plugin-pukiwiki-like-linker/src/index.js
  71. 0 8
      packages/plugin-pukiwiki-like-linker/src/meta.js
  72. 1 1
      packages/slack/package.json
  73. 2 2
      packages/slackbot-proxy/package.json
  74. 18 17
      packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts
  75. 5 3
      packages/slackbot-proxy/src/middlewares/slack-to-growi/join-to-conversation.ts
  76. 4 2
      packages/ui/package.json
  77. 42 71
      yarn.lock

+ 13 - 9
.github/release-drafter.yml

@@ -20,23 +20,27 @@ change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add
 autolabeler:
   - label: 'feature'
     branch:
-      - '/feat\/.+/'
+      - '/^feat\/.+/'
   - label: 'improvement'
     branch:
-      - '/imprv\/.+/'
+      - '/^imprv\/.+/'
   - label: 'bug'
     branch:
-      - '/fix\/.+/'
+      - '/^fix\/.+/'
     title:
-      - '/fix/i'
+      - '/^fix/i'
   - label: 'support'
     branch:
-      - '/support\/.+/'
+      - '/^support\/.+/'
     title:
-      - '/chore/i'
-      - '/ci/i'
-      - '/docs/i'
-      - '/test/i'
+      - '/^ci/i'
+      - '/^docs/i'
+      - '/^test/i'
+  - label: 'exclude from changelog'
+    branch:
+      - '/^chore\/.+/'
+    title:
+      - '/^chore/i'
 
 exclude-labels:
   - 'exclude from changelog'

+ 2 - 1
.github/workflows/ci-slackbot-proxy.yml

@@ -5,7 +5,8 @@ on:
     branches-ignore:
       - release/**
       - rc/**
-      - tmp/**
+      - chore/**
+      - support/prepare-v**
 
 jobs:
 

+ 2 - 1
.github/workflows/ci.yml

@@ -5,7 +5,8 @@ on:
     branches-ignore:
       - release/**
       - rc/**
-      - tmp/**
+      - chore/**
+      - support/prepare-v**
 
 jobs:
 

+ 8 - 4
.github/workflows/draft-release.yml

@@ -12,13 +12,11 @@ jobs:
     runs-on: ubuntu-latest
 
     outputs:
-      RELEASE_VERSION: ${{ steps.package-json.outputs.packageVersion }}
+      CURRENT_VERSION: ${{ steps.package-json.outputs.packageVersion }}
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
 
     steps:
       - uses: actions/checkout@v2
-        with:
-          fetch-depth: 0
 
       - name: Retrieve information from package.json
         uses: myrotvorets/info-from-package-json-action@0.0.2
@@ -46,6 +44,12 @@ jobs:
         with:
           fetch-depth: 0
 
+      - name: Get release version
+        id: release-version
+        run: |
+          RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
+          echo ::set-output name=RELEASE_VERSION::$RELEASE_VERSION
+
       - name: Create/Update Pull Request
         uses: bakunyo/git-pr-release-action@master
         env:
@@ -53,6 +57,6 @@ jobs:
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current
           GIT_PR_RELEASE_BRANCH_STAGING: master
           GIT_PR_RELEASE_TEMPLATE: .github/git-pr-release-template.erb
-          GIT_PR_RELEASE_TITLE: Release ${{ needs.update-release-draft.outputs.RELEASE_VERSION }}
+          GIT_PR_RELEASE_TITLE: Release v${{ steps.release-version.outputs.RELEASE_VERSION }}
           GIT_PR_RELEASE_BODY: ${{ needs.update-release-draft.outputs.RELEASE_DRAFT_BODY }}
 

+ 6 - 2
.github/workflows/pr-to-master.yml

@@ -13,7 +13,7 @@ jobs:
   auto-labeling:
     runs-on: ubuntu-latest
 
-    if: github.event.pull_request.merged == true
+    if: ${{ !contains(github.event.pull_request.labels.*.name, 'exclude from changelog') }}
 
     steps:
       - uses: release-drafter/release-drafter@v5
@@ -24,6 +24,11 @@ jobs:
 
   check-title:
     runs-on: ubuntu-latest
+
+    if: |
+      (!contains( github.event.pull_request.labels.*.name, 'exclude from changelog' ) &&
+        !startsWith( github.ref, 'refs/heads/chore/' ))
+
     steps:
       - uses: amannn/action-semantic-pull-request@v3.4.2
         with:
@@ -37,6 +42,5 @@ jobs:
             docs
             test
           requireScope: false
-          validateSingleCommit: true
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 10 - 2
.github/workflows/release.yml

@@ -34,7 +34,8 @@ jobs:
 
     - name: Bump versions
       run: |
-        node ./bin/bump-versions -i patch
+        node ./bin/github-actions/bump-versions -i patch
+        sh ./packages/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@0.0.2
@@ -46,6 +47,13 @@ jobs:
         latest-version: v${{ steps.package-json.outputs.packageVersion }}
         release-notes: ${{ github.event.pull_request.body }}
 
+    - name: Update README.md for docker image
+      working-directory: ./packages/app
+      run: |
+        sh ./bin/github-actions/update-readme.sh
+      env:
+        RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
+
     - name: Commit, Tag and Push
       uses: stefanzweifel/git-auto-commit-action@v4
       with:
@@ -87,7 +95,7 @@ jobs:
 
     - name: Bump versions for next RC
       run: |
-        node ./bin/bump-versions -i prerelease
+        node ./bin/github-actions/bump-versions -i prerelease
 
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@0.0.2

File diff suppressed because it is too large
+ 518 - 343
CHANGELOG.md


+ 0 - 55
bin/bump-versions/step/printHelp.js

@@ -1,55 +0,0 @@
-import runStep from 'shipjs/src/step/runStep';
-import { print } from 'shipjs/src/util';
-import { bold, underline } from 'shipjs/src/color';
-
-export default () =>
-  runStep({}, () => {
-    const indent = (line) => `\t${line}`;
-
-    const help = `--help`;
-    const dir = `--dir ${underline('PATH')}`;
-    const increment = `--increment ${underline('LEVEL')}`;
-    const preId = `--preid ${underline('IDENTIFIER')}`;
-    const dryRun = `--dry-run`;
-    const all = [help, dir, increment, preId, dryRun]
-      .map((x) => `[${x}]`)
-      .join(' ');
-
-    const messages = [
-      bold('NAME'),
-      indent('bump-versions - Bump versions of packages.'),
-      '',
-      bold('USAGE'),
-      indent(`node ./bin/github-actions/bump-versions ${all}`),
-      '',
-      bold('OPTIONS'),
-      indent(`-h, ${help}`),
-      indent('  Print this help'),
-      '',
-      indent(`-d, ${dir}`),
-      indent(
-        `  Specify the ${underline(
-          'PATH'
-        )} of the repository (default: the current directory).`
-      ),
-      '',
-      indent(`-i, ${increment}`),
-      indent(
-        `  Specify the ${underline(
-          'LEVEL'
-        )} for semver.inc() to increment a version (default: 'patch').`
-      ),
-      '',
-      indent(`${preId}`),
-      indent(
-        `  Specify the ${underline(
-          'IDENTIFIER'
-        )} for semver.inc() with 'prerelease' type (default: 'RC').`
-      ),
-      '',
-      indent(`-D, ${dryRun}`),
-      indent('  Displays the steps without actually doing them.'),
-      '',
-    ];
-    print(messages.join('\n'));
-  });

+ 0 - 0
bin/bump-versions/README.md → bin/github-actions/bump-versions/README.md


+ 4 - 2
bin/bump-versions/cli.js → bin/github-actions/bump-versions/cli.js

@@ -6,10 +6,12 @@ export async function cli(argv) {
   try {
     const opts = parseArgs(argSpec, argv);
     await fn(opts);
-  } catch (error) {
+  }
+  catch (error) {
     if (error.code === 'ARG_UNKNOWN_OPTION') {
       print(error);
-    } else {
+    }
+    else {
       throw error;
     }
   }

+ 8 - 8
bin/bump-versions/flow/bump-versions.js → bin/github-actions/bump-versions/flow/bump-versions.js

@@ -25,14 +25,13 @@ async function bumpVersions({
   }
 
   const config = await loadConfig(dir, 'bump-versions.config');
-  
+
   // get current version
   const { monorepo } = config;
-  const currentVersion =
-    monorepo && monorepo.mainVersionFile
-      ? getCurrentVersion(dir, monorepo.mainVersionFile)
-      : getCurrentVersion(dir);
-  
+  const currentVersion = monorepo && monorepo.mainVersionFile
+    ? getCurrentVersion(dir, monorepo.mainVersionFile)
+    : getCurrentVersion(dir);
+
   // determine next version
   let nextVersion = semver.inc(currentVersion, increment, preid); // set preid if type is 'prerelease'
   nextVersion = await confirmNextVersion({
@@ -47,8 +46,9 @@ async function bumpVersions({
   const updateVersionFn = monorepo
     ? updateVersionMonorepo
     : updateVersion;
-  await updateVersionFn({ config, nextVersion, releaseType, dir, dryRun });
-  installDependencies({ config, dir, dryRun });
+  await updateVersionFn({
+    config, nextVersion, releaseType, dir, dryRun,
+  });
 }
 
 const arg = {

+ 6 - 2
bin/bump-versions/index.js → bin/github-actions/bump-versions/index.js

@@ -1,12 +1,16 @@
 #!/usr/bin/env node
 
+// eslint-disable-next-line no-global-assign
 require = require('esm')(module);
+
 (async function() {
   try {
     process.env.SHIPJS = true;
     await require('./cli').cli(process.argv);
-  } catch (e) {
+  }
+  catch (e) {
+    // eslint-disable-next-line no-console
     console.error(e);
     process.exit(1);
   }
-})();
+}());

+ 54 - 0
bin/github-actions/bump-versions/step/printHelp.js

@@ -0,0 +1,54 @@
+import runStep from 'shipjs/src/step/runStep';
+import { print } from 'shipjs/src/util';
+import { bold, underline } from 'shipjs/src/color';
+
+export default () => runStep({}, () => {
+  const indent = line => `\t${line}`;
+
+  const help = '--help';
+  const dir = `--dir ${underline('PATH')}`;
+  const increment = `--increment ${underline('LEVEL')}`;
+  const preId = `--preid ${underline('IDENTIFIER')}`;
+  const dryRun = '--dry-run';
+  const all = [help, dir, increment, preId, dryRun]
+    .map(x => `[${x}]`)
+    .join(' ');
+
+  const messages = [
+    bold('NAME'),
+    indent('bump-versions - Bump versions of packages.'),
+    '',
+    bold('USAGE'),
+    indent(`node ./bin/github-actions/bump-versions ${all}`),
+    '',
+    bold('OPTIONS'),
+    indent(`-h, ${help}`),
+    indent('  Print this help'),
+    '',
+    indent(`-d, ${dir}`),
+    indent(
+      `  Specify the ${underline(
+        'PATH',
+      )} of the repository (default: the current directory).`,
+    ),
+    '',
+    indent(`-i, ${increment}`),
+    indent(
+      `  Specify the ${underline(
+        'LEVEL',
+      )} for semver.inc() to increment a version (default: 'patch').`,
+    ),
+    '',
+    indent(`${preId}`),
+    indent(
+      `  Specify the ${underline(
+        'IDENTIFIER',
+      )} for semver.inc() with 'prerelease' type (default: 'RC').`,
+    ),
+    '',
+    indent(`-D, ${dryRun}`),
+    indent('  Displays the steps without actually doing them.'),
+    '',
+  ];
+  print(messages.join('\n'));
+});

+ 6 - 6
bump-versions.config.js

@@ -1,12 +1,12 @@
+/*
+ * Reference: https://community.algolia.com/shipjs/
+ */
 module.exports = {
   monorepo: {
-    mainVersionFile: 'package.json',
+    mainVersionFile: 'lerna.json',
     packagesToBump: [
-      'packages/app',
-      'packages/core',
-      'packages/slack',
-      'packages/ui',
-      'packages/plugin-*',
+      './',
+      'packages/*',
     ],
   },
 };

+ 2 - 2
lerna.json

@@ -1,8 +1,8 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
+  "version": "4.4.3-RC.0",
   "packages": [
     "packages/*"
-  ],
-  "version": "independent"
+  ]
 }

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -31,7 +31,7 @@
   "scripts": {
     "start": "yarn app:server",
     "prestart": "yarn app:build",
-    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-*",
+    "app:build": "yarn lerna run build",
     "app:server": "yarn lerna run server --scope @growi/app",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",

+ 2 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.3\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.3-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.4\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.4-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md

+ 6 - 6
packages/app/config/migrate.js

@@ -5,9 +5,13 @@
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
 
+import mongoose from 'mongoose';
+
+import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+
 const { URL } = require('url');
 
-const { getMongoUri } = require('~/server/util/mongoose-utils');
+initMongooseGlobalSettings();
 
 const mongoUri = getMongoUri();
 
@@ -17,11 +21,7 @@ const url = new URL(mongoUri);
 const mongodb = {
   url: mongoUri,
   databaseName: url.pathname.substring(1), // omit heading slash
-  options: {
-    useNewUrlParser: true, // removes a deprecation warning when connecting
-    useUnifiedTopology: true,
-    useFindAndModify: false,
-  },
+  options: mongoOptions,
 };
 
 module.exports = {

+ 0 - 2
packages/app/docker/Dockerfile

@@ -110,9 +110,7 @@ RUN tar cf packages.tar \
   package.json \
   yarn.lock \
   tsconfig.base.json \
-  packages/app/package.json \
   packages/app/config \
-  packages/app/dist \
   packages/app/public \
   packages/app/resource \
   packages/app/tmp \

+ 5 - 5
packages/app/docker/README.md

@@ -2,7 +2,7 @@
 GROWI Official docker image
 ========================
 
-[![Actions Status](https://github.com/weseek/growi/workflows/Release%20Docker%20Images/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
+[![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
 
 ![GROWI-x-docker](https://user-images.githubusercontent.com/1638767/38307565-105956e2-384f-11e8-8534-b1128522d68d.png)
 
@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.3.0`, `4.3`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.0/docker/Dockerfile)
-* [`4.3.0-nocdn`, `4.3-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.0/docker/Dockerfile)
-* [`4.2.0`, `4.2` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
-* [`4.2.0-nocdn`, `4.2-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
+* [`4.4.2`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.2/docker/Dockerfile)
+* [`4.4.2-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.2/docker/Dockerfile)
+* [`4.3.3`, `4.3` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
+* [`4.3.3-nocdn`, `4.3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 
 
 What is GROWI?

+ 11 - 13
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -10,7 +10,7 @@
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server.json",
     "clean": "npx shx rm -rf dist transpiled",
     "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
-    "postbuild": "npx shx mv transpiled/src dist && npx shx rm -rf transpiled",
+    "postbuild": "npx shx mv transpiled/src dist && npx shx cp -r src/server/views dist/server/ && npx shx rm -rf transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
@@ -18,7 +18,7 @@
     "dev": "run-p dev:client dev:server",
     "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
     "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
-    "dev:server": "yarn cross-env NODE_ENV=development yarn ts-node-dev src/server/app.ts --expose_gc",
+    "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect --expose-gc -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
     "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
     "predev:server": "yarn cross-env NODE_ENV=development yarn migrate",
     "//// for CI": "",
@@ -44,8 +44,7 @@
     "migrate:status": "yarn ts-node node_modules/.bin/migrate-mongo status -f config/migrate.js",
     "migrate:up": "yarn ts-node node_modules/.bin/migrate-mongo up -f config/migrate.js",
     "migrate:down": "yarn ts-node node_modules/.bin/migrate-mongo down -f config/migrate.js",
-    "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only",
-    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config --inspect --transpile-only"
+    "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
   },
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
@@ -55,11 +54,10 @@
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/plugin-attachment-refs": "^4.4.0-RC",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.0-RC",
-    "@growi/plugin-lsx": "^4.4.0-RC",
-    "@growi/slack": "^4.4.0-RC",
-    "@kobalab/socket.io-session": "^1.0.3",
+    "@growi/plugin-attachment-refs": "^4.4.3-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.3-RC.0",
+    "@growi/plugin-lsx": "^4.4.3-RC.0",
+    "@growi/slack": "^4.4.3-RC.0",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
     "@slack/events-api": "^3.0.0",
@@ -133,7 +131,7 @@
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
-    "socket.io": "^4.0.0",
+    "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
@@ -155,7 +153,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.0-RC",
+    "@growi/ui": "^4.4.3-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -222,7 +220,7 @@
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^4.0.0",
+    "socket.io-client": "^4.2.0",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",

+ 1 - 1
packages/app/src/components/PagePathHierarchicalLink.jsx

@@ -46,7 +46,7 @@ const PagePathHierarchicalLink = (props) => {
   const RootElm = ({ children }) => {
     return props.isInnerElem
       ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link d-inline-block text-break">{children}</span>;
+      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
   };
 
   return (

+ 1 - 1
packages/app/src/components/Sidebar/CustomSidebar.jsx

@@ -61,7 +61,7 @@ const CustomSidebar = (props) => {
           Custom Sidebar
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
-        <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={fetchDataAndRenderHtml}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload-cs" onClick={fetchDataAndRenderHtml}>
           <i className="icon icon-reload"></i>
         </button>
       </div>

+ 138 - 30
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -10,6 +10,8 @@ import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
 
+import FootstampIcon from '../FootstampIcon';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
@@ -17,6 +19,106 @@ import { toastError } from '~/client/util/apiNotification';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
 const logger = loggerFactory('growi:History');
+
+function PageItemLower({ page }) {
+  return (
+    <div className="d-flex justify-content-between grw-recent-changes-item-lower pt-1">
+      <div className="d-flex">
+        <div className="footstamp-icon mr-1 d-inline-block"><FootstampIcon /></div>
+        <div className="mr-2 grw-list-counts d-inline-block">{page.seenUsers.length}</div>
+        <div className="icon-bubble mr-1 d-inline-block"></div>
+        <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
+      </div>
+      <div className="grw-formatted-distance-date small mt-auto">
+        <FormattedDistanceDate id={page._id} date={page.updatedAt} />
+      </div>
+    </div>
+  );
+}
+PageItemLower.propTypes = {
+  page: PropTypes.any,
+};
+function LargePageItem({ page }) {
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+  const FormerLink = () => (
+    <div className="grw-page-path-text-muted-container small">
+      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
+    </div>
+  );
+
+  let locked;
+  if (page.grant !== 1) {
+    locked = <span><i className="icon-lock ml-2" /></span>;
+  }
+
+  const tags = page.tags;
+  const tagElements = tags.map((tag) => {
+    return (
+      <a key={tag.name} href={`/_search?q=tag:${tag.name}`} className="grw-tag-label badge badge-secondary mr-2 small">
+        {tag.name}
+      </a>
+    );
+  });
+
+  return (
+    <li className="list-group-item py-3 px-0">
+      <div className="d-flex w-100">
+        <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
+        <div className="flex-grow-1 ml-2">
+          { !dPagePath.isRoot && <FormerLink /> }
+          <h5 className="my-2">
+            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+            {locked}
+          </h5>
+          <div className="grw-tag-labels mt-1 mb-2">
+            { tagElements }
+          </div>
+          <PageItemLower page={page} />
+        </div>
+      </div>
+    </li>
+  );
+}
+LargePageItem.propTypes = {
+  page: PropTypes.any,
+};
+
+function SmallPageItem({ page }) {
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+  const FormerLink = () => (
+    <div className="grw-page-path-text-muted-container small">
+      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
+    </div>
+  );
+
+  let locked;
+  if (page.grant !== 1) {
+    locked = <span><i className="icon-lock ml-2" /></span>;
+  }
+
+  return (
+    <li className="list-group-item py-2 px-0">
+      <div className="d-flex w-100">
+        <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
+        <div className="flex-grow-1 ml-2">
+          { !dPagePath.isRoot && <FormerLink /> }
+          <h5 className="my-0">
+            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+            {locked}
+          </h5>
+          <PageItemLower page={page} />
+        </div>
+      </div>
+    </li>
+  );
+}
+SmallPageItem.propTypes = {
+  page: PropTypes.any,
+};
 class RecentChanges extends React.Component {
 
   static propTypes = {
@@ -26,10 +128,16 @@ class RecentChanges extends React.Component {
 
   constructor(props) {
     super(props);
-
+    this.state = {
+      isRecentChangesSidebarSmall: false,
+    };
     this.reloadData = this.reloadData.bind(this);
   }
 
+  componentWillMount() {
+    this.retrieveSizePreferenceFromLocalStorage();
+  }
+
   async componentDidMount() {
     this.reloadData();
   }
@@ -46,36 +154,22 @@ class RecentChanges extends React.Component {
     }
   }
 
-  PageItem = ({ page }) => {
-    const dPagePath = new DevidedPagePath(page.path, false, true);
-    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    const FormerLink = () => (
-      <div className="grw-page-path-text-muted-container small">
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
-      </div>
-    );
+  retrieveSizePreferenceFromLocalStorage() {
+    if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
+      this.setState({
+        isRecentChangesSidebarSmall: true,
+      });
+    }
+  }
 
-    return (
-      <li className="list-group-item p-2">
-        <div className="d-flex w-100">
-          <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
-          <div className="flex-grow-1 ml-2">
-            { !dPagePath.isRoot && <FormerLink /> }
-            <h5 className="mb-1">
-              <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            </h5>
-            <div className="text-right small">
-              <FormattedDistanceDate id={page.id} date={page.updatedAt} />
-            </div>
-          </div>
-        </div>
-      </li>
-    );
+  changeSizeHandler = (e) => {
+    this.setState({
+      isRecentChangesSidebarSmall: e.target.checked,
+    });
+    window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
   }
 
   render() {
-    const { PageItem } = this;
     const { t } = this.props;
     const { recentlyUpdatedPages } = this.props.appContainer.state;
 
@@ -84,13 +178,26 @@ class RecentChanges extends React.Component {
         <div className="grw-sidebar-content-header p-3 d-flex">
           <h3 className="mb-0">{t('Recent Changes')}</h3>
           {/* <h3 className="mb-0">{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
-          <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={this.reloadData}>
+          <button type="button" className="btn btn-sm ml-auto grw-btn-reload-rc" onClick={this.reloadData}>
             <i className="icon icon-reload"></i>
           </button>
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-2">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={this.state.isRecentChangesSidebarSmall}
+              onChange={e => this.setState({ isRecentChangesSidebarSmall: e.target.checked })}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
+          </div>
         </div>
-        <div className="grw-sidebar-content-body p-3">
+        <div className="grw-sidebar-content-body grw-recent-changes p-3">
           <ul className="list-group list-group-flush">
-            { recentlyUpdatedPages.map(page => <PageItem key={page.id} page={page} />) }
+            {recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
+              ? <SmallPageItem key={page._id} page={page} />
+              : <LargePageItem key={page._id} page={page} />))}
           </ul>
         </div>
       </>
@@ -104,4 +211,5 @@ class RecentChanges extends React.Component {
  */
 const RecentChangesWrapper = withUnstatedContainers(RecentChanges, [AppContainer]);
 
+
 export default withTranslation()(RecentChangesWrapper);

+ 33 - 0
packages/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js

@@ -0,0 +1,33 @@
+import mongoose from 'mongoose';
+
+import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
+import { getModelSafely } from '~/server/util/mongoose-utils';
+import config from '^/config/migrate';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-value');
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    // Add columns + set all default commands if supportedCommandsForBroadcastUse column does not exist
+    const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
+
+    // Add togetter command if supportedCommandsForBroadcastUse already exists
+    const slackAppIntegrations = await SlackAppIntegration.find();
+    slackAppIntegrations.forEach(async(doc) => {
+      if (!doc.supportedCommandsForSingleUse.includes('togetter')) {
+        doc.supportedCommandsForSingleUse.push('togetter');
+      }
+      await doc.save();
+    });
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // no rollback
+  },
+};

+ 0 - 1
packages/app/src/server/.node-dev.json

@@ -2,7 +2,6 @@
   "ignore": [
     "package.json",
     "public/manifest.json",
-    "config/env.",
     "config/webpack."
   ]
 }

+ 3 - 1
packages/app/src/server/console.js

@@ -2,7 +2,7 @@ const repl = require('repl');
 const fs = require('fs');
 const path = require('path');
 const mongoose = require('mongoose');
-const { getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
 
 const models = require('./models');
 
@@ -32,6 +32,8 @@ fs.readFile(replHistoryPath, 'utf8', (err, data) => {
 replServer.context.mongoose = mongoose;
 replServer.context.models = models;
 
+initMongooseGlobalSettings();
+
 mongoose.connect(getMongoUri(), mongoOptions)
   .then(() => {
     replServer.context.db = mongoose.connection.db;

+ 4 - 2
packages/app/src/server/crowi/index.js

@@ -9,7 +9,7 @@ import CdnResourcesService from '~/services/cdn-resources-service';
 import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
-import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 import ConfigManager from '../service/config-manager';
@@ -35,7 +35,7 @@ function Crowi() {
   this.publicDir = path.join(projectRoot, 'public') + sep;
   this.resourceDir = path.join(projectRoot, 'resource') + sep;
   this.localeDir = path.join(this.resourceDir, 'locales') + sep;
-  this.viewsDir = path.join(projectRoot, 'src', 'server', 'views') + sep;
+  this.viewsDir = path.resolve(__dirname, '../views') + sep;
   this.tmpDir = path.join(projectRoot, 'tmp') + sep;
   this.cacheDir = path.join(this.tmpDir, 'cache');
 
@@ -214,6 +214,8 @@ Crowi.prototype.setupDatabase = function() {
   // mongoUri = mongodb://user:password@host/dbname
   const mongoUri = getMongoUri();
 
+  initMongooseGlobalSettings();
+
   return mongoose.connect(mongoUri, mongoOptions);
 };
 

+ 2 - 1
packages/app/src/server/middlewares/admin-required.js

@@ -4,7 +4,8 @@ const logger = loggerFactory('growi:middleware:admin-required');
 
 module.exports = (crowi, fallback = null) => {
 
-  return async(req, res, next) => {
+  return function(req, res, next) {
+
     if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user.admin) {
         return next();

+ 1 - 1
packages/app/src/server/models/page.js

@@ -772,7 +772,7 @@ module.exports = function(crowi) {
     // find
     builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
     builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-    const pages = await builder.query.exec('find');
+    const pages = await builder.query.lean().exec('find');
 
     const result = {
       pages, totalCount, offset: opt.offset, limit: opt.limit,

+ 1 - 0
packages/app/src/server/models/slack-app-integration.js

@@ -1,5 +1,6 @@
 const crypto = require('crypto');
 const mongoose = require('mongoose');
+const { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } = require('@growi/slack');
 
 
 const schema = new mongoose.Schema({

+ 72 - 4
packages/app/src/server/routes/apiv3/pages.js

@@ -4,6 +4,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const express = require('express');
 const pathUtils = require('growi-commons').pathUtils;
+const mongoose = require('mongoose');
 
 const { body } = require('express-validator');
 const { query } = require('express-validator');
@@ -22,6 +23,36 @@ const LIMIT_FOR_LIST = 10;
  *    name: Pages
  */
 
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Tags:
+ *        description: Tags
+ *        type: array
+ *        items:
+ *          $ref: '#/components/schemas/Tag/properties/name'
+ *        example: ['daily', 'report', 'tips']
+ *
+ *      Tag:
+ *        description: Tag
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: tag ID
+ *            example: 5e2d6aede35da4004ef7e0b7
+ *          name:
+ *            type: string
+ *            description: tag name
+ *            example: daily
+ *          count:
+ *            type: number
+ *            description: Count of tagged pages
+ *            example: 3
+ */
+
 /**
  * @swagger
  *
@@ -76,7 +107,7 @@ const LIMIT_FOR_LIST = 10;
  *          path:
  *            type: string
  *            description: page path
- *            example: /
+ *            example: /Sandbox/Math
  *          redirectTo:
  *            type: string
  *            description: redirect path
@@ -176,7 +207,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /pages/create:
+   *    /pages:
    *      post:
    *        tags: [Pages]
    *        operationId: createPage
@@ -193,6 +224,14 @@ module.exports = (crowi) => {
    *                    $ref: '#/components/schemas/Page/properties/path'
    *                  grant:
    *                    $ref: '#/components/schemas/Page/properties/grant'
+   *                  grantUserGroupId:
+   *                    type: string
+   *                    description: UserGroup ID
+   *                    example: 5ae5fccfc5577b0004dbd8ab
+   *                  pageTags:
+   *                    type: array
+   *                    items:
+   *                      $ref: '#/components/schemas/Tag'
    *                required:
    *                  - body
    *                  - path
@@ -203,8 +242,17 @@ module.exports = (crowi) => {
    *              application/json:
    *                schema:
    *                  properties:
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
+   *                    data:
+   *                      type: object
+   *                      properties:
+   *                        page:
+   *                          $ref: '#/components/schemas/Page'
+   *                        tags:
+   *                          type: array
+   *                          items:
+   *                            $ref: '#/components/schemas/Tags'
+   *                        revision:
+   *                          $ref: '#/components/schemas/Revision'
    *          409:
    *            description: page path is already existed
    */
@@ -311,6 +359,26 @@ module.exports = (crowi) => {
         }
       });
 
+      const PageTagRelation = mongoose.model('PageTagRelation');
+      const ids = result.pages.map((page) => { return page._id });
+      const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
+
+      // { pageId: [{ tag }, ...] }
+      const relationsMap = new Map();
+      // increment relationsMap
+      relations.forEach((relation) => {
+        const pageId = relation.relatedPage.toString();
+        if (!relationsMap.has(pageId)) {
+          relationsMap.set(pageId, []);
+        }
+        relationsMap.get(pageId).push(relation.relatedTag);
+      });
+      // add tags to each page
+      result.pages.forEach((page) => {
+        const pageId = page._id.toString();
+        page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
+      });
+
       return res.apiv3(result);
     }
     catch (err) {

+ 15 - 18
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -21,7 +21,6 @@ const logger = loggerFactory('growi:routes:apiv3:slack-integration-settings');
 
 const router = express.Router();
 
-const OFFICIAL_SLACKBOT_PROXY_URI = 'https://slackbot-proxy.growi.org';
 
 /**
  * @swagger
@@ -109,17 +108,12 @@ module.exports = (crowi) => {
       'slackbot:withoutProxy:commandPermission': null,
     };
 
-    // set url if officialBot is specified
-    if (initializedType === SlackbotType.OFFICIAL) {
-      params['slackbot:proxyUri'] = OFFICIAL_SLACKBOT_PROXY_URI;
-    }
-
     return updateSlackBotSettings(params);
   }
 
   async function getConnectionStatusesFromProxy(tokens) {
     const csv = tokens.join(',');
-    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyUri');
+    const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
 
     const result = await axios.get(urljoin(proxyUri, '/g2s/connection-status'), {
       headers: {
@@ -132,7 +126,7 @@ module.exports = (crowi) => {
   }
 
   async function requestToProxyServer(token, method, endpoint, body) {
-    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyUri');
+    const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
     if (proxyUri == null) {
       throw new Error('Proxy URL is not registered');
     }
@@ -627,15 +621,18 @@ module.exports = (crowi) => {
         { new: true },
       );
 
-      await requestToProxyServer(
-        slackAppIntegration.tokenGtoP,
-        'put',
-        '/g2s/supported-commands',
-        {
-          permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
-          permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
-        },
-      );
+      const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
+      if (proxyUri != null) {
+        await requestToProxyServer(
+          slackAppIntegration.tokenGtoP,
+          'put',
+          '/g2s/supported-commands',
+          {
+            supportedCommandsForBroadcastUse: slackAppIntegration.supportedCommandsForBroadcastUse,
+            supportedCommandsForSingleUse: slackAppIntegration.supportedCommandsForSingleUse,
+          },
+        );
+      }
 
       return res.apiv3({});
     }
@@ -667,7 +664,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(msg, 'not-proxy-type'), 400);
     }
 
-    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyUri');
+    const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
     if (proxyUri == null) {
       return res.apiv3Err(new ErrorV3('Proxy URL is null.', 'not-proxy-Uri'), 400);
     }

+ 8 - 4
packages/app/src/server/service/search.js

@@ -34,26 +34,30 @@ class SearchService {
   }
 
   get isSearchboxEnabled() {
-    return this.configManager.getConfig('crowi', 'app:searchboxSslUrl') != null;
+    const uri = this.configManager.getConfig('crowi', 'app:searchboxSslUrl');
+    return uri != null && uri.length > 0;
   }
 
   get isElasticsearchEnabled() {
-    return this.configManager.getConfig('crowi', 'app:elasticsearchUri') != null;
+    const uri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
+    return uri != null && uri.length > 0;
   }
 
   generateDelegator() {
     logger.info('Initializing search delegator');
 
     if (this.isSearchboxEnabled) {
-      logger.info('Searchbox is enabled');
       const SearchboxDelegator = require('./search-delegator/searchbox');
+      logger.info('Searchbox is enabled');
       return new SearchboxDelegator(this.configManager, this.crowi.socketIoService);
     }
     if (this.isElasticsearchEnabled) {
-      logger.info('Elasticsearch (not Searchbox) is enabled');
       const ElasticsearchDelegator = require('./search-delegator/elasticsearch');
+      logger.info('Elasticsearch (not Searchbox) is enabled');
       return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
     }
+
+    logger.info('No elasticsearch URI is specified so that full text search is disabled.');
   }
 
   registerUpdateEvent() {

+ 21 - 2
packages/app/src/server/service/slack-integration.ts

@@ -16,6 +16,7 @@ import { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:SlackBotService');
 
+const OFFICIAL_SLACKBOT_PROXY_URI = 'https://slackbot-proxy.growi.org';
 
 type S2sMessageForSlackIntegration = S2sMessage & { updatedAt: Date };
 
@@ -106,6 +107,25 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     return true;
   }
 
+  get proxyUriForCurrentType(): string {
+    const currentBotType = this.configManager.getConfig('crowi', 'slackbot:currentBotType');
+
+    // TODO assert currentBotType is not null and CUSTOM_WITHOUT_PROXY
+
+    let proxyUri: string;
+
+    switch (currentBotType) {
+      case SlackbotType.OFFICIAL:
+        proxyUri = OFFICIAL_SLACKBOT_PROXY_URI;
+        break;
+      default:
+        proxyUri = this.configManager.getConfig('crowi', 'slackbot:proxyUri');
+        break;
+    }
+
+    return proxyUri;
+  }
+
   /**
    * generate WebClient instance for CUSTOM_WITHOUT_PROXY type
    */
@@ -171,8 +191,7 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     this.isCheckTypeValid();
 
     // connect to proxy
-    const proxyServerUri = this.configManager.getConfig('crowi', 'slackbot:proxyUri');
-    const serverUri = new URL('/g2s', proxyServerUri);
+    const serverUri = new URL('/g2s', this.proxyUriForCurrentType);
     const headers = {
       'x-growi-gtop-tokens': slackAppIntegration.tokenGtoP,
     };

+ 19 - 12
packages/app/src/server/service/socket-io.js

@@ -3,7 +3,6 @@ import loggerFactory from '~/utils/logger';
 const socketIo = require('socket.io');
 const expressSession = require('express-session');
 const passport = require('passport');
-const socketioSession = require('@kobalab/socket.io-session');
 
 const logger = loggerFactory('growi:service:socket-io');
 
@@ -24,7 +23,8 @@ class SocketIoService {
     return (this.io != null);
   }
 
-  attachServer(server) {
+  // Since the Order is important, attachServer() should be async
+  async attachServer(server) {
     this.io = socketIo(server, {
       transports: ['websocket'],
     });
@@ -34,12 +34,12 @@ class SocketIoService {
 
     // setup middlewares
     // !!CAUTION!! -- ORDER IS IMPORTANT
-    this.setupSessionMiddleware();
-    this.setupLoginRequiredMiddleware();
-    this.setupAdminRequiredMiddleware();
-    this.setupCheckConnectionLimitsMiddleware();
+    await this.setupSessionMiddleware();
+    await this.setupLoginRequiredMiddleware();
+    await this.setupAdminRequiredMiddleware();
+    await this.setupCheckConnectionLimitsMiddleware();
 
-    this.setupStoreGuestIdEventHandler();
+    await this.setupStoreGuestIdEventHandler();
   }
 
   getDefaultSocket() {
@@ -59,13 +59,20 @@ class SocketIoService {
 
   /**
    * use passport session
-   * @see https://qiita.com/kobalab/items/083e507fb01159fe9774
+   * @see https://socket.io/docs/v4/middlewares/#Compatibility-with-Express-middleware
    */
   setupSessionMiddleware() {
-    const sessionMiddleware = socketioSession(expressSession(this.crowi.sessionConfig), passport);
-    this.io.use(sessionMiddleware.express_session);
-    this.io.use(sessionMiddleware.passport_initialize);
-    this.io.use(sessionMiddleware.passport_session);
+    const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
+
+    this.io.use(wrap(expressSession(this.crowi.sessionConfig)));
+    this.io.use(wrap(passport.initialize()));
+    this.io.use(wrap(passport.session()));
+
+    // express and passport session on main socket doesn't shared to child namespace socket
+    // need to define the session for specific namespace
+    this.getAdminSocket().use(wrap(expressSession(this.crowi.sessionConfig)));
+    this.getAdminSocket().use(wrap(passport.initialize()));
+    this.getAdminSocket().use(wrap(passport.session()));
   }
 
   /**

+ 11 - 1
packages/app/src/server/util/mongoose-utils.ts

@@ -2,6 +2,13 @@ import mongoose, {
   Model, Document, ConnectionOptions, Schema,
 } from 'mongoose';
 
+export const initMongooseGlobalSettings = (): void => {
+  // supress deprecation warnings
+  // see: https://mongoosejs.com/docs/deprecations.html
+  mongoose.set('useFindAndModify', false);
+  mongoose.set('useCreateIndex', true);
+};
+
 export const getMongoUri = (): string => {
   const { env } = process;
 
@@ -26,6 +33,9 @@ export const getOrCreateModel = <Interface, Method>(modelName: string, schema: S
   return mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName, schema);
 };
 
+// supress deprecation warnings
+// see: https://mongoosejs.com/docs/deprecations.html
 export const mongoOptions: ConnectionOptions = {
-  useFindAndModify: false,
+  useNewUrlParser: true,
+  useUnifiedTopology: true,
 };

+ 49 - 0
packages/app/src/styles/_recent-changes.scss

@@ -0,0 +1,49 @@
+.grw-sidebar-content-header {
+  .grw-btn-reload-rc {
+    font-size: 18px;
+  }
+
+  .grw-recent-changes-resize-button {
+    font-size: 12px;
+    line-height: normal;
+    transform: translateY(6px);
+
+    .custom-control-label::before {
+      padding-left: 16px;
+      content: 'L';
+    }
+
+    .custom-control-input:checked + .custom-control-label::before {
+      padding-left: 5px;
+      content: 'S';
+    }
+  }
+}
+
+.list-group {
+  .list-group-item {
+    .grw-recent-changes-item-lower {
+      height: 17.5px;
+    }
+    .footstamp-icon {
+      svg {
+        width: 14px;
+        height: 14px;
+        transform: translateY(-3.5px);
+      }
+    }
+
+    .grw-list-counts {
+      height: 14px;
+      font-size: 12px;
+    }
+
+    .grw-formatted-distance-date {
+      font-size: 10px;
+    }
+
+    .icon-lock {
+      font-size: 14px;
+    }
+  }
+}

+ 4 - 0
packages/app/src/styles/_sidebar.scss

@@ -133,6 +133,10 @@
   .grw-drawer-toggler {
     display: none; // invisible in default
   }
+
+  .grw-sidebar-content-header {
+    min-width: $grw-sidebar-content-min-width + 20px;
+  }
 }
 
 // Dock Mode

+ 8 - 2
packages/app/src/styles/_tag.scss

@@ -6,9 +6,9 @@
 
 .grw-tag-labels {
   .grw-tag-label {
-    margin-left: 1px;
     font-size: 12px;
-    border-radius: $border-radius-xl;
+    font-weight: normal;
+    border-radius: $border-radius-sm;
   }
 }
 
@@ -17,3 +17,9 @@
     height: auto;
   }
 }
+
+.grw-recent-changes {
+  .grw-tag-label {
+    font-size: 10px;
+  }
+}

+ 1 - 0
packages/app/src/styles/style-app.scss

@@ -59,6 +59,7 @@
 @import 'page';
 @import 'page-presentation';
 @import 'page-history';
+@import 'recent-changes';
 @import 'search';
 @import 'shortcuts';
 @import 'sidebar';

+ 1 - 1
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -12,7 +12,7 @@ $border-color-table: $gray-200 !default;
 $color-table-hover: $color-table !default;
 $bgcolor-table-hover: rgba(black, 0.075) !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
-$color-tags: $gray-500 !default;
+$color-tags: $secondary !default;
 $bgcolor-tags: $gray-200 !default;
 $border-color-global: $gray-300 !default;
 $border-color-toc: $border-color-global !default;

+ 54 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -15,6 +15,8 @@ $bordercolor-nav-tabs-hover: $gray-200 $gray-200 $bordercolor-nav-tabs !default;
 $color-nav-tabs-link-active: $gray-600 !default;
 $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
 $color-seen-user: #549c79 !default;
+$reload-btn-rc-color: $gray-500;
+$reload-btn-cs-color: $gray-500;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -263,6 +265,58 @@ ul.pagination {
       }
     }
   }
+
+  .grw-sidebar-content-header {
+    .grw-btn-reload-rc {
+      color: $reload-btn-rc-color;
+    }
+    .grw-btn-reload-cs {
+      color: $reload-btn-cs-color;
+    }
+
+    .grw-recent-changes-resize-button {
+      .custom-control-label::before {
+        background-color: $primary;
+      }
+
+      .custom-control-label::after {
+        background-color: $bgcolor-global;
+      }
+
+      .custom-control-input:not(:checked) + .custom-control-label::before {
+        color: $bgcolor-global;
+      }
+
+      .custom-control-input:checked + .custom-control-label::before {
+        color: $bgcolor-global;
+        background-color: $primary;
+        // border-color: $primary !important;
+      }
+      .custom-control-input:checked + .custom-control-label::after {
+        color: $bgcolor-global;
+      }
+    }
+  }
+
+  .grw-recent-changes {
+    .list-group {
+      .list-group-item {
+        background-color: transparent;
+
+        .icon-lock {
+          color: $color-link;
+        }
+
+        .grw-recent-changes-item-lower {
+          color: $gray-500;
+
+          svg {
+            fill: $gray-500;
+          }
+        }
+      }
+    }
+  }
 }
 
 /*

+ 4 - 0
packages/app/src/test/config/migrate.test.js

@@ -13,9 +13,12 @@ describe('config/migrate.js', () => {
   `('returns', ({ MONGO_URI, expectedUrl, expectedDbName }) => {
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
 
+      const initMongooseGlobalSettingsMock = jest.fn();
+
       // mock for mongoose-utils
       jest.doMock('~/server/util/mongoose-utils', () => {
         return {
+          initMongooseGlobalSettings: initMongooseGlobalSettingsMock,
           getMongoUri: () => {
             return MONGO_URI;
           },
@@ -26,6 +29,7 @@ describe('config/migrate.js', () => {
 
       jest.dontMock('~/server/util/mongoose-utils');
 
+      expect(initMongooseGlobalSettingsMock).toHaveBeenCalledTimes(1);
       expect(mongoUri).toBe(MONGO_URI);
       expect(mongodb.url).toBe(expectedUrl);
       expect(mongodb.databaseName).toBe(expectedDbName);

+ 3 - 1
packages/app/src/test/global-setup.js

@@ -9,7 +9,7 @@ import 'tsconfig-paths/register';
 
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 
 // check env
 if (process.env.NODE_ENV !== 'test') {
@@ -21,6 +21,8 @@ if (process.env.NODE_ENV !== 'test') {
 // const { getInstance } = require('./setup-crowi');
 
 module.exports = async() => {
+  initMongooseGlobalSettings();
+
   await mongoose.connect(getMongoUri(), mongoOptions);
 
   // drop database

+ 2 - 1
packages/app/src/test/setup.js

@@ -7,13 +7,14 @@
 
 const mongoose = require('mongoose');
 
-const { getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
 
 mongoose.Promise = global.Promise;
 
 jest.setTimeout(30000); // default 5000
 
 beforeAll(async() => {
+  initMongooseGlobalSettings();
   await mongoose.connect(getMongoUri(), mongoOptions);
 });
 

+ 5 - 4
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [
@@ -8,7 +8,9 @@
   ],
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
-  "files": ["dist"],
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
@@ -19,6 +21,5 @@
     "test": "jest --verbose"
   },
   "dependencies": {},
-  "devDependencies": {
-  }
+  "devDependencies": {}
 }

+ 0 - 1
packages/plugin-attachment-refs/index.js

@@ -1 +0,0 @@
-module.exports = require('./src/meta');

+ 6 - 5
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [
@@ -9,7 +9,9 @@
   ],
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
-  "files": ["dist"],
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
@@ -22,7 +24,6 @@
   "dependencies": {
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
-    "growi-commons": "^5.0.4",
     "http-errors": "^1.8.0",
     "react-images": "~1.0.0",
     "react-motion": "^0.5.2",
@@ -31,7 +32,7 @@
   "devDependencies": {
     "npm-run-all": "^4.1.5",
     "prettier-stylelint": "^0.4.2",
-    "react": "^16.4.1",
-    "react-dom": "^16.4.1"
+    "react": "^16.8.3",
+    "react-dom": "^16.8.3"
   }
 }

+ 0 - 0
packages/plugin-attachment-refs/src/meta.js → packages/plugin-attachment-refs/src/index.js


+ 25 - 10
packages/plugin-attachment-refs/src/server/routes/refs.js

@@ -6,9 +6,20 @@ const { OptionParser } = customTagUtils;
 
 const logger = loggerFactory('growi-plugin:attachment-refs:routes:refs');
 
+
+const loginRequiredFallback = (req, res) => {
+  return res.status(403).send('login required');
+};
+
+
 module.exports = (crowi) => {
   const express = crowi.require('express');
   const mongoose = crowi.require('mongoose');
+
+  const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
+  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
+  const { serializeUserSecurely } = crowi.require('../models/serializers/user-serializer');
+
   const router = express.Router();
 
   const ObjectId = mongoose.Types.ObjectId;
@@ -68,7 +79,7 @@ module.exports = (crowi) => {
   /**
    * return an Attachment model
    */
-  router.get('/ref', async(req, res) => {
+  router.get('/ref', accessTokenParser, loginRequired, async(req, res) => {
     const user = req.user;
     const { pagePath, fileNameOrId } = req.query;
     // eslint-disable-next-line no-unused-vars
@@ -87,12 +98,6 @@ module.exports = (crowi) => {
       return;
     }
 
-    let creatorPopulateOpt;
-    // set populate option for backward compatibility against to GROWI <= v4.0.x
-    if (User.IMAGE_POPULATION != null) {
-      creatorPopulateOpt = User.IMAGE_POPULATION;
-    }
-
     // convert ObjectId
     const orConditions = [{ originalName: fileNameOrId }];
     if (ObjectId.isValid(fileNameOrId)) {
@@ -104,7 +109,7 @@ module.exports = (crowi) => {
         page: page._id,
         $or: orConditions,
       })
-      .populate({ path: 'creator', select: User.USER_PUBLIC_FIELDS, populate: creatorPopulateOpt });
+      .populate('creator');
 
     // not found
     if (attachment == null) {
@@ -122,13 +127,16 @@ module.exports = (crowi) => {
       return;
     }
 
+    // serialize User data
+    attachment.creator = serializeUserSecurely(attachment.creator);
+
     res.status(200).send({ attachment });
   });
 
   /**
    * return a list of Attachment
    */
-  router.get('/refs', async(req, res) => {
+  router.get('/refs', accessTokenParser, loginRequired, async(req, res) => {
     const user = req.user;
     const { prefix, pagePath } = req.query;
     const options = JSON.parse(req.query.options);
@@ -198,9 +206,16 @@ module.exports = (crowi) => {
     }
 
     const attachments = await query
-      .populate({ path: 'creator', select: User.USER_PUBLIC_FIELDS })
+      .populate('creator')
       .exec();
 
+    // serialize User data
+    attachments.forEach((doc) => {
+      if (doc.creator != null && doc.creator instanceof User) {
+        doc.creator = serializeUserSecurely(doc.creator);
+      }
+    });
+
     res.status(200).send({ attachments });
   });
 

+ 5 - 4
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [
@@ -9,7 +9,9 @@
   ],
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
-  "files": ["dist"],
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
@@ -19,8 +21,7 @@
     "lint": "run-p lint:*",
     "test": ""
   },
-  "dependencies": {
-  },
+  "dependencies": {},
   "devDependencies": {
     "react": "^16.8.3",
     "react-dom": "^16.8.3"

+ 2 - 0
packages/plugin-lsx/src/client-entry.js

@@ -1,9 +1,11 @@
+import { LsxLogoutInterceptor } from './client/js/util/Interceptor/LsxLogoutInterceptor';
 import { LsxPreRenderInterceptor } from './client/js/util/Interceptor/LsxPreRenderInterceptor';
 import { LsxPostRenderInterceptor } from './client/js/util/Interceptor/LsxPostRenderInterceptor';
 
 export default (appContainer) => {
   // add interceptors
   appContainer.interceptorManager.addInterceptors([
+    new LsxLogoutInterceptor(),
     new LsxPreRenderInterceptor(),
     new LsxPostRenderInterceptor(appContainer),
   ]);

+ 7 - 2
packages/plugin-lsx/src/client/css/index.css

@@ -1,6 +1,7 @@
 .lsx .page-list-ul > li > a:not(:hover) {
   text-decoration: none;
 }
+
 .lsx .lsx-page-not-exist {
   opacity: 0.6;
 }
@@ -10,6 +11,10 @@
 }
 
 @keyframes lsx-fadeIn {
-  0% {opacity: .2}
-  100% {opacity: .9}
+  0% {
+    opacity: 0.2;
+  }
+  100% {
+    opacity: 0.9;
+  }
 }

+ 76 - 46
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -9,64 +9,86 @@ import { pathUtils } from 'growi-commons';
 import styles from '../../css/index.css';
 
 import { LsxContext } from '../util/LsxContext';
-import { LsxCacheHelper } from '../util/LsxCacheHelper';
+import { TagCacheManagerFactory } from '../util/TagCacheManagerFactory';
 import { PageNode } from './PageNode';
 import { LsxListView } from './LsxPageList/LsxListView';
 
-
 export class Lsx extends React.Component {
 
   constructor(props) {
     super(props);
 
     this.state = {
-      isLoading: true,
+      isLoading: false,
       isError: false,
+      isCacheExists: false,
       nodeTree: undefined,
       errorMessage: '',
     };
+
+    this.tagCacheManager = TagCacheManagerFactory.getInstance();
   }
 
-  componentWillMount() {
-    const lsxContext = this.props.lsxContext;
-    lsxContext.parse();
+  async componentWillMount() {
+    const { lsxContext, forceToFetchData } = this.props;
 
-    // check cache exists
-    if (this.props.lsxStateCache) {
+    // get state object cache
+    const stateCache = this.retrieveDataFromCache();
+
+    if (stateCache != null) {
       this.setState({
-        isLoading: false,
-        nodeTree: this.props.lsxStateCache.nodeTree,
-        isError: this.props.lsxStateCache.isError,
-        errorMessage: this.props.lsxStateCache.errorMessage,
+        isCacheExists: true,
+        nodeTree: stateCache.nodeTree,
+        isError: stateCache.isError,
+        errorMessage: stateCache.errorMessage,
       });
-      return; // go to render()
+
+      // switch behavior by forceToFetchData
+      if (!forceToFetchData) {
+        return; // go to render()
+      }
     }
 
+    lsxContext.parse();
+    this.setState({ isLoading: true });
+
     // add slash ensure not to forward match to another page
     // ex: '/Java/' not to match to '/JavaScript'
     const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
 
-    this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options })
-      .then((res) => {
-        if (res.ok) {
-          const nodeTree = this.generatePageNodeTree(pagePath, res.pages);
-          this.setState({ nodeTree });
-        }
-        else {
-          return Promise.reject(res.error);
-        }
-      })
-      .catch((error) => {
-        this.setState({ isError: true, errorMessage: error.message });
-      })
-      // finally
-      .then(() => {
-        this.setState({ isLoading: false });
-
-        // store to sessionStorage
-        const cacheKey = LsxCacheHelper.generateCacheKeyFromContext(lsxContext);
-        LsxCacheHelper.cacheState(cacheKey, this.state);
+    try {
+      const res = await this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options });
+
+      if (res.ok) {
+        const nodeTree = this.generatePageNodeTree(pagePath, res.pages);
+        this.setState({ nodeTree });
+      }
+    }
+    catch (error) {
+      this.setState({ isError: true, errorMessage: error.message });
+    }
+    finally {
+      this.setState({ isLoading: false });
+
+      // store to sessionStorage
+      this.tagCacheManager.cacheState(lsxContext, this.state);
+    }
+  }
+
+  retrieveDataFromCache() {
+    const { lsxContext } = this.props;
+
+    // get state object cache
+    const stateCache = this.tagCacheManager.getStateCache(lsxContext);
+
+    // instanciate PageNode
+    if (stateCache != null && stateCache.nodeTree != null) {
+      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
+        return PageNode.instanciateFrom(obj);
       });
+    }
+
+    return stateCache;
   }
 
   /**
@@ -170,16 +192,11 @@ export class Lsx extends React.Component {
 
   renderContents() {
     const lsxContext = this.props.lsxContext;
+    const {
+      isLoading, isError, isCacheExists, nodeTree,
+    } = this.state;
 
-    if (this.state.isLoading) {
-      return (
-        <div className="text-muted">
-          <i className="fa fa-spinner fa-pulse mr-1"></i>
-          <span className="lsx-blink">{lsxContext.tagExpression}</span>
-        </div>
-      );
-    }
-    if (this.state.isError) {
+    if (isError) {
       return (
         <div className="text-warning">
           <i className="fa fa-exclamation-triangle fa-fw"></i>
@@ -187,9 +204,22 @@ export class Lsx extends React.Component {
         </div>
       );
     }
-    // render tree
 
-    return <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} />;
+
+    return (
+      <div className={isLoading ? 'lsx-blink' : ''}>
+        { isLoading && (
+          <div className="text-muted">
+            <i className="fa fa-spinner fa-pulse mr-1"></i>
+            {lsxContext.tagExpression}
+            { isCacheExists && <small>&nbsp;(Showing cache..)</small> }
+          </div>
+        ) }
+        { nodeTree && (
+          <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} />
+        ) }
+      </div>
+    );
 
   }
 
@@ -201,7 +231,7 @@ export class Lsx extends React.Component {
 
 Lsx.propTypes = {
   appContainer: PropTypes.object.isRequired,
-
   lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
-  lsxStateCache: PropTypes.object,
+
+  forceToFetchData: PropTypes.bool,
 };

+ 10 - 6
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxListView.jsx

@@ -10,7 +10,9 @@ export class LsxListView extends React.Component {
   render() {
     const listView = this.props.nodeTree.map((pageNode) => {
       return (
-        <LsxPage key={pageNode.pagePath} depth={1}
+        <LsxPage
+          key={pageNode.pagePath}
+          depth={1}
           pageNode={pageNode}
           lsxContext={this.props.lsxContext}
         />
@@ -19,12 +21,14 @@ export class LsxListView extends React.Component {
 
     // no contents
     if (this.props.nodeTree.length === 0) {
-      return <div className="text-muted">
-        <small>
-          <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+      return (
+        <div className="text-muted">
+          <small>
+            <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
             $lsx(<a href={this.props.lsxContext.pagePath}>{this.props.lsxContext.pagePath}</a>) has no contents
-        </small>
-      </div>;
+          </small>
+        </div>
+      );
     }
 
     return (

+ 3 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx

@@ -57,7 +57,9 @@ export class LsxPage extends React.Component {
     if (this.state.hasChildren) {
       const pages = pageNode.children.map((pageNode) => {
         return (
-          <LsxPage key={pageNode.pagePath} depth={this.props.depth + 1}
+          <LsxPage
+            key={pageNode.pagePath}
+            depth={this.props.depth + 1}
             pageNode={pageNode}
             lsxContext={this.props.lsxContext}
           />

+ 1 - 0
packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx

@@ -22,6 +22,7 @@ export class PagePathWrapper extends React.Component {
 PagePathWrapper.propTypes = {
   pagePath: PropTypes.string.isRequired,
   isExists: PropTypes.bool.isRequired,
+  excludePathString: PropTypes.string,
 };
 
 PagePathWrapper.defaultProps = {

+ 33 - 0
packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js

@@ -0,0 +1,33 @@
+import { BasicInterceptor } from 'growi-commons';
+
+import { TagCacheManagerFactory } from '../TagCacheManagerFactory';
+
+/**
+ * The interceptor for lsx
+ *
+ *  replace lsx tag to a React target element
+ */
+export class LsxLogoutInterceptor extends BasicInterceptor {
+
+  /**
+   * @inheritdoc
+   */
+  isInterceptWhen(contextName) {
+    return (
+      contextName === 'logout'
+    );
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async process(contextName, ...args) {
+    const context = Object.assign(args[0]); // clone
+
+    TagCacheManagerFactory.getInstance().clearAllStateCaches();
+
+    // resolve
+    return context;
+  }
+
+}

+ 12 - 17
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

@@ -3,8 +3,8 @@ import ReactDOM from 'react-dom';
 
 import { BasicInterceptor } from 'growi-commons';
 
+import { LsxContext } from '../LsxContext';
 import { Lsx } from '../../components/Lsx';
-import { LsxCacheHelper } from '../LsxCacheHelper';
 
 /**
  * The interceptor for lsx
@@ -31,35 +31,30 @@ export class LsxPostRenderInterceptor extends BasicInterceptor {
   /**
    * @inheritdoc
    */
-  process(contextName, ...args) {
+  async process(contextName, ...args) {
     const context = Object.assign(args[0]); // clone
 
-    if (context.lsxContextMap == null) {
-      return Promise.resolve();
-    }
+    const isPreview = (contextName === 'postRenderPreviewHtml');
 
     // forEach keys of lsxContextMap
-    Object.keys(context.lsxContextMap).forEach((renderId) => {
-      const elem = document.getElementById(renderId);
+    Object.keys(context.lsxContextMap).forEach((domId) => {
+      const elem = document.getElementById(domId);
 
       if (elem) {
-        // get LsxContext instance from context
-        const lsxContext = context.lsxContextMap[renderId];
+        // instanciate LsxContext from context
+        const lsxContext = new LsxContext(context.lsxContextMap[domId] || {});
+        lsxContext.fromPagePath = context.currentPagePath;
 
-        // check cache exists
-        const cacheKey = LsxCacheHelper.generateCacheKeyFromContext(lsxContext);
-        const lsxStateCache = LsxCacheHelper.getStateCache(cacheKey);
-
-        this.renderReactDOM(lsxContext, lsxStateCache, elem);
+        this.renderReactDOM(lsxContext, elem, isPreview);
       }
     });
 
-    return Promise.resolve();
+    return;
   }
 
-  renderReactDOM(lsxContext, lsxStateCache, elem) {
+  renderReactDOM(lsxContext, elem, isPreview) {
     ReactDOM.render(
-      <Lsx appContainer={this.appContainer} lsxContext={lsxContext} lsxStateCache={lsxStateCache} />,
+      <Lsx appContainer={this.appContainer} lsxContext={lsxContext} forceToFetchData={!isPreview} />,
       elem,
     );
   }

+ 33 - 62
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js

@@ -1,7 +1,5 @@
-import { BasicInterceptor } from 'growi-commons';
-
-import { LsxContext } from '../LsxContext';
-import { LsxCacheHelper } from '../LsxCacheHelper';
+import ReactDOM from 'react-dom';
+import { customTagUtils, BasicInterceptor } from 'growi-commons';
 
 /**
  * The interceptor for lsx
@@ -10,6 +8,12 @@ import { LsxCacheHelper } from '../LsxCacheHelper';
  */
 export class LsxPreRenderInterceptor extends BasicInterceptor {
 
+  constructor() {
+    super();
+
+    this.previousPreviewContext = null;
+  }
+
   /**
    * @inheritdoc
    */
@@ -23,75 +27,42 @@ export class LsxPreRenderInterceptor extends BasicInterceptor {
   /**
    * @inheritdoc
    */
-  process(contextName, ...args) {
+  isProcessableParallel() {
+    return false;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async process(contextName, ...args) {
     const context = Object.assign(args[0]); // clone
     const parsedHTML = context.parsedHTML;
-    const currentPagePath = context.currentPagePath;
-    this.initializeCache(contextName);
-
-    context.lsxContextMap = {};
-
-    // TODO retrieve from args for interceptor
-    const fromPagePath = currentPagePath;
 
-    // see: https://regex101.com/r/NQq3s9/7
-    const pattern = /\$lsx(\((.*?)\)(?=\s|<br>|\$lsx))|\$lsx(\((.*)\)(?!\s|<br>|\$lsx))/g;
-    context.parsedHTML = parsedHTML.replace(pattern, (all, group1, group2, group3, group4) => {
-      const tagExpression = all;
-      let lsxArgs = group2 || group4 || '';
-      lsxArgs = lsxArgs.trim();
+    const tagPattern = /ls|lsx/;
+    const result = customTagUtils.findTagAndReplace(tagPattern, parsedHTML);
 
-      // create contexts
-      const lsxContext = new LsxContext();
-      lsxContext.currentPagePath = currentPagePath;
-      lsxContext.tagExpression = tagExpression;
-      lsxContext.fromPagePath = fromPagePath;
-      lsxContext.lsxArgs = lsxArgs;
+    context.parsedHTML = result.html;
+    context.lsxContextMap = result.tagContextMap;
 
-      const renderId = `lsx-${this.createRandomStr(8)}`;
-
-      context.lsxContextMap[renderId] = lsxContext;
-
-      // return replace strings
-      return this.createReactTargetDom(renderId);
-    });
+    // unmount
+    if (contextName === 'preRenderPreviewHtml') {
+      this.unmountPreviousReactDOMs(context);
+    }
 
     // resolve
-    return Promise.resolve(context);
+    return context;
   }
 
-  createReactTargetDom(renderId) {
-    return `<div id="${renderId}"></div>`;
-  }
-
-  /**
-   * initialize cache
-   *  when contextName is 'preRenderHtml'         -> clear cache
-   *  when contextName is 'preRenderPreviewHtml'  -> doesn't clear cache
-   *
-   * @param {string} contextName
-   *
-   * @memberOf LsxPreRenderInterceptor
-   */
-  initializeCache(contextName) {
-    if (contextName === 'preRenderHtml') {
-      LsxCacheHelper.clearAllStateCaches();
+  unmountPreviousReactDOMs(newContext) {
+    if (this.previousPreviewContext != null) {
+      // forEach keys of lsxContextMap
+      Object.keys(this.previousPreviewContext.lsxContextMap).forEach((domId) => {
+        const elem = document.getElementById(domId);
+        ReactDOM.unmountComponentAtNode(elem);
+      });
     }
-  }
 
-  /**
-   * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
-   *
-   * @param {number} length
-   * @return random strings
-   */
-  createRandomStr(length) {
-    const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
-    let generated = '';
-    for (let i = 0; i < length; i++) {
-      generated += bag[Math.floor(Math.random() * bag.length)];
-    }
-    return generated;
+    this.previousPreviewContext = newContext;
   }
 
 }

+ 0 - 87
packages/plugin-lsx/src/client/js/util/LsxCacheHelper.js

@@ -1,87 +0,0 @@
-import { PageNode } from '../components/PageNode';
-
-export class LsxCacheHelper {
-
-  /**
-   * @private
-   */
-  static retrieveFromSessionStorage() {
-    return JSON.parse(sessionStorage.getItem('lsx-cache')) || {};
-  }
-
-  /**
-   * stringify and save obj
-   *
-   * @static
-   * @param {object} cacheObj
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static saveToSessionStorage(cacheObj) {
-    sessionStorage.setItem('lsx-cache', JSON.stringify(cacheObj));
-  }
-
-  /**
-   * generate cache key for storing to storage
-   *
-   * @static
-   * @param {LsxContext} lsxContext
-   * @returns
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static generateCacheKeyFromContext(lsxContext) {
-    return `${lsxContext.fromPagePath}__${lsxContext.lsxArgs}`;
-  }
-
-  /**
-   *
-   *
-   * @static
-   * @param {string} key
-   * @returns
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static getStateCache(key) {
-    const cacheObj = LsxCacheHelper.retrieveFromSessionStorage();
-    const stateCache = cacheObj[key];
-
-    if (stateCache != null && stateCache.nodeTree != null) {
-      // instanciate PageNode
-      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
-        return PageNode.instanciateFrom(obj);
-      });
-    }
-
-    return stateCache;
-  }
-
-  /**
-   * store state object of React Component with specified key
-   *
-   * @static
-   * @param {string} key
-   * @param {object} lsxState state object of React Component
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static cacheState(key, lsxState) {
-    const cacheObj = LsxCacheHelper.retrieveFromSessionStorage();
-    cacheObj[key] = lsxState;
-
-    LsxCacheHelper.saveToSessionStorage(cacheObj);
-  }
-
-  /**
-   * clear all state caches
-   *
-   * @static
-   *
-   * @memberOf LsxCacheHelper
-   */
-  static clearAllStateCaches() {
-    LsxCacheHelper.saveToSessionStorage({});
-  }
-
-}

+ 23 - 83
packages/plugin-lsx/src/client/js/util/LsxContext.js

@@ -1,19 +1,23 @@
 import * as url from 'url';
 
-import { pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from 'growi-commons';
 
-export class LsxContext {
+const { TagContext, ArgsParser, OptionParser } = customTagUtils;
+
+export class LsxContext extends TagContext {
+
+  /**
+   * @param {object|TagContext|LsxContext} initArgs
+   */
+  constructor(initArgs) {
+    super(initArgs);
 
-  constructor() {
-    this.currentPagePath = null;
-    this.tagExpression = null;
     this.fromPagePath = null;
-    this.lsxArgs = null;
 
     // initialized after parse()
     this.isParsed = null;
     this.pagePath = null;
-    this.options = null;
+    this.options = {};
   }
 
   parse() {
@@ -21,40 +25,17 @@ export class LsxContext {
       return;
     }
 
-    // initialize
-    let specifiedPath;
-    this.options = {};
-
-    if (this.lsxArgs.length > 0) {
-      const splittedArgs = this.lsxArgs.split(',');
-      let firstArgsKey; let
-        firstArgsValue;
-
-      splittedArgs.forEach((arg, index) => {
-        const trimedArg = arg.trim();
+    const parsedResult = ArgsParser.parse(this.args);
+    this.options = parsedResult.options;
 
-        // parse string like 'key1=value1, key2=value2, ...'
-        // see https://regex101.com/r/pYHcOM/1
-        const match = trimedArg.match(/([^=]+)=?(.+)?/);
-        const key = match[1];
-        const value = match[2] || true;
-        this.options[key] = value;
-
-        if (index === 0) {
-          firstArgsKey = key;
-          firstArgsValue = value;
-        }
-      });
-
-      // determine specifiedPath
-      // order:
-      //   1: lsx(prefix=..., ...)
-      //   2: lsx(firstArgs, ...)
-      //   3: fromPagePath
-      specifiedPath = this.options.prefix
-          || ((firstArgsValue === true) ? firstArgsKey : undefined)
-          || this.fromPagePath;
-    }
+    // determine specifiedPath
+    // order:
+    //   1: lsx(prefix=..., ...)
+    //   2: lsx(firstArgs, ...)
+    //   3: fromPagePath
+    const specifiedPath = this.options.prefix
+        || ((parsedResult.firstArgsValue === true) ? parsedResult.firstArgsKey : undefined)
+        || this.fromPagePath;
 
     // resolve pagePath
     //   when `fromPagePath`=/hoge and `specifiedPath`=./fuga,
@@ -71,51 +52,10 @@ export class LsxContext {
   }
 
   getOptDepth() {
-    // eslint-disable-next-line eqeqeq
-    if (this.options.depth == undefined) {
-      return undefined;
-    }
-    return this.parseNum(this.options.depth);
-  }
-
-  parseNum(str) {
-    // eslint-disable-next-line eqeqeq
-    if (str == undefined) {
-      return undefined;
-    }
-
-    // see: https://regex101.com/r/w4KCwC/3
-    const match = str.match(/^(-?[0-9]+)(([:+]{1})(-?[0-9]+)?)?$/);
-    if (!match) {
+    if (this.options.depth === undefined) {
       return undefined;
     }
-
-    // determine start
-    let start;
-    let end;
-
-    // has operator
-    // eslint-disable-next-line eqeqeq
-    if (match[3] != undefined) {
-      start = +match[1];
-      const operator = match[3];
-
-      // determine end
-      if (operator === ':') {
-        end = +match[4] || -1; // set last(-1) if undefined
-      }
-      else if (operator === '+') {
-        end = +match[4] || 0; // plus zero if undefined
-        end += start;
-      }
-    }
-    // don't have operator
-    else {
-      start = 1;
-      end = +match[1];
-    }
-
-    return { start, end };
+    return OptionParser.parseRange(this.options.depth);
   }
 
 }

+ 39 - 0
packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js

@@ -0,0 +1,39 @@
+import { TagCacheManager } from 'growi-commons';
+
+const LSX_STATE_CACHE_NS = 'lsx-state-cache';
+
+
+// validate growi-commons version
+function validateGrowiCommonsVersion() {
+  // TagCacheManager was created on growi-commons@4.0.7
+  if (TagCacheManager == null) {
+    throw new Error(
+      'This version of \'growi-plugin-lsx\' requires \'growi-commons >= 4.0.7\'.\n'
+      + 'To resolve this, please process  either a) or b).\n'
+      + '\n'
+      + 'a) Use \'growi-plugin-lsx@3.0.0\'\n'
+      + 'b) Edit \'package.json\' of growi and upgrade \'growi-commons\' to v4.0.7 or above.',
+    );
+  }
+}
+
+
+let _instance;
+export class TagCacheManagerFactory {
+
+  static getInstance() {
+    validateGrowiCommonsVersion();
+
+    if (_instance == null) {
+      // create generateCacheKey implementation
+      const generateCacheKey = (lsxContext) => {
+        return `${lsxContext.fromPagePath}__${lsxContext.args}`;
+      };
+
+      _instance = new TagCacheManager(LSX_STATE_CACHE_NS, generateCacheKey);
+    }
+
+    return _instance;
+  }
+
+}

+ 11 - 1
packages/plugin-lsx/src/index.js

@@ -1 +1,11 @@
-module.exports = require('./meta');
+const isProd = process.env.NODE_ENV === 'production';
+
+module.exports = {
+  pluginSchemaVersion: 4,
+  serverEntries: [
+    isProd ? 'dist/cjs/server-entry.js' : 'src/server-entry.js',
+  ],
+  clientEntries: [
+    'src/client-entry.js',
+  ],
+};

+ 0 - 11
packages/plugin-lsx/src/meta.js

@@ -1,11 +0,0 @@
-const isProd = process.env.NODE_ENV === 'production';
-
-module.exports = {
-  pluginSchemaVersion: 4,
-  serverEntries: [
-    isProd ? 'dist/cjs/server-entry.js' : 'src/server-entry.js',
-  ],
-  clientEntries: [
-    'src/client-entry.js',
-  ],
-};

+ 10 - 8
packages/plugin-lsx/src/server/routes/index.js

@@ -1,11 +1,13 @@
+
+const loginRequiredFallback = (req, res) => {
+  return res.status(403).send('login required');
+};
+
 module.exports = (crowi, app) => {
   const lsx = require('./lsx')(crowi, app);
-  // const middleware = crowi.require('../util/middlewares');
-  // const debug = require('debug')('growi-plugin:lsx:routes');
-  // const loginRequired = middleware.loginRequired;
-  // const accessTokenParser = middleware.accessTokenParser(crowi, app);
-  // const csrf = middleware.csrfVerify(crowi, app);
-
-  // app.get('/_api/plugins/lsx', accessTokenParser , loginRequired(crowi, app) , lsx.renderHtml);
-  app.get('/_api/plugins/lsx', lsx.listPages);
+
+  const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
+  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
+
+  app.get('/_api/plugins/lsx', accessTokenParser, loginRequired, lsx.listPages);
 };

+ 81 - 15
packages/plugin-lsx/src/server/routes/lsx.js

@@ -2,6 +2,9 @@ const { customTagUtils } = require('growi-commons');
 
 const { OptionParser } = customTagUtils;
 
+
+const DEFAULT_PAGES_NUM = 50;
+
 class Lsx {
 
   /**
@@ -16,6 +19,11 @@ class Lsx {
    * @memberOf Lsx
    */
   static addDepthCondition(query, pagePath, optionsDepth) {
+    // when option strings is 'depth=', the option value is true
+    if (optionsDepth == null || optionsDepth === true) {
+      throw new Error('The value of depth option is invalid.');
+    }
+
     const range = OptionParser.parseRange(optionsDepth);
     const start = range.start;
     const end = range.end;
@@ -40,12 +48,21 @@ class Lsx {
    * @static
    * @param {any} query
    * @param {any} pagePath
-   * @param {any} optionsNum
+   * @param {number|string} optionsNum
    * @returns
    *
    * @memberOf Lsx
    */
   static addNumCondition(query, pagePath, optionsNum) {
+    // when option strings is 'num=', the option value is true
+    if (optionsNum == null || optionsNum === true) {
+      throw new Error('The value of num option is invalid.');
+    }
+
+    if (typeof optionsNum === 'number') {
+      return query.limit(optionsNum);
+    }
+
     const range = OptionParser.parseRange(optionsNum);
     const start = range.start;
     const end = range.end;
@@ -60,6 +77,47 @@ class Lsx {
     return query.skip(skip).limit(limit);
   }
 
+  /**
+   * add filter condition that filter fetched pages
+   *
+   * @static
+   * @param {any} query
+   * @param {any} pagePath
+   * @param {any} optionsFilter
+   * @param {boolean} isExceptFilter
+   * @returns
+   *
+   * @memberOf Lsx
+   */
+  static addFilterCondition(query, pagePath, optionsFilter, isExceptFilter = false) {
+    // when option strings is 'filter=', the option value is true
+    if (optionsFilter == null || optionsFilter === true) {
+      throw new Error('filter option require value in regular expression.');
+    }
+
+    let filterPath = '';
+    if (optionsFilter.charAt(0) === '^') {
+      // move '^' to the first of path
+      filterPath = new RegExp(`^${pagePath}${optionsFilter.slice(1, optionsFilter.length)}`);
+    }
+    else {
+      filterPath = new RegExp(`^${pagePath}.*${optionsFilter}`);
+    }
+
+    if (isExceptFilter) {
+      return query.and({
+        path: { $not: filterPath },
+      });
+    }
+    return query.and({
+      path: filterPath,
+    });
+  }
+
+  static addExceptCondition(query, pagePath, optionsFilter) {
+    return this.addFilterCondition(query, pagePath, optionsFilter, true);
+  }
+
   /**
    * add sort condition(sort key & sort order)
    *
@@ -75,7 +133,10 @@ class Lsx {
    *
    * @memberOf Lsx
    */
-  static addSortCondition(query, pagePath, optionsSort = 'path', optionsReverse) {
+  static addSortCondition(query, pagePath, optionsSortArg, optionsReverse) {
+    // init sort key
+    const optionsSort = optionsSortArg || 'path';
+
     // the default sort order
     let isReversed = false;
 
@@ -126,19 +187,18 @@ module.exports = (crowi, app) => {
     }
 
     const builder = new Page.PageQueryBuilder(baseQuery);
-    builder.addConditionToListWithDescendants(pagePath, {})
+    if (builder.addConditionToListOnlyDescendants == null) { // for Backward compatibility (<= GROWI v4.0.x)
+      builder.addConditionToListWithDescendants(pagePath);
+    }
+    else {
+      builder.addConditionToListOnlyDescendants(pagePath);
+    }
+
+    builder
       .addConditionToExcludeTrashed()
       .addConditionToExcludeRedirect();
 
-    let promisifiedBuilder = Promise.resolve(builder);
-
-    if (user != null) {
-      const UserGroupRelation = crowi.model('UserGroupRelation');
-      const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-      promisifiedBuilder = builder.addConditionToFilteringByViewer(user, userGroups);
-    }
-
-    return promisifiedBuilder;
+    return Page.addConditionToFilteringByViewerForList(builder, user);
   }
 
   actions.listPages = async(req, res) => {
@@ -154,10 +214,16 @@ module.exports = (crowi, app) => {
       if (options.depth != null) {
         query = Lsx.addDepthCondition(query, pagePath, options.depth);
       }
-      // num
-      if (options.num != null) {
-        query = Lsx.addNumCondition(query, pagePath, options.num);
+      // filter
+      if (options.filter != null) {
+        query = Lsx.addFilterCondition(query, pagePath, options.filter);
       }
+      if (options.except != null) {
+        query = Lsx.addExceptCondition(query, pagePath, options.except);
+      }
+      // num
+      const optionsNum = options.num || DEFAULT_PAGES_NUM;
+      query = Lsx.addNumCondition(query, pagePath, optionsNum);
       // sort
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);
 

+ 4 - 2
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [
@@ -9,7 +9,9 @@
   ],
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
-  "files": ["dist"],
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",

+ 8 - 1
packages/plugin-pukiwiki-like-linker/src/index.js

@@ -1 +1,8 @@
-module.exports = require('./meta');
+module.exports = {
+  pluginSchemaVersion: 4,
+  serverEntries: [
+  ],
+  clientEntries: [
+    'src/client-entry.js',
+  ],
+};

+ 0 - 8
packages/plugin-pukiwiki-like-linker/src/meta.js

@@ -1,8 +0,0 @@
-module.exports = {
-  pluginSchemaVersion: 4,
-  serverEntries: [
-  ],
-  clientEntries: [
-    'src/client-entry.js',
-  ],
-};

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "1.0.3",
+  "version": "4.4.3-RC.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -23,7 +23,7 @@
   "// comments for dependencies": {},
   "dependencies": {
     "@godaddy/terminus": "^4.8.0",
-    "@growi/slack": "^4.4.0-RC",
+    "@growi/slack": "^4.4.3-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 18 - 17
packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts

@@ -1,21 +1,25 @@
 import { AuthorizeResult, InstallationQuery } from '@slack/oauth';
 import {
-  IMiddleware, Inject, Middleware, Req, Res,
+  IMiddleware, Inject, Middleware, Next, Req, Res,
 } from '@tsed/common';
 
 import Logger from 'bunyan';
 
+import createError from 'http-errors';
+
 import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallerService } from '~/services/InstallerService';
 import loggerFactory from '~/utils/logger';
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const logger = loggerFactory('@growi/slackbot-proxy:middlewares:authorizer');
+
 
 const getCommonMiddleware = (query:InstallationQuery<boolean>, installerService:InstallerService, logger:Logger) => {
-  return async(req: SlackOauthReq, res: Res): Promise<void|Res> => {
+  return async(req: SlackOauthReq, res: Res, next: Next): Promise<void|Res> => {
 
     if (query.teamId == null && query.enterpriseId == null) {
-      res.writeHead(400, 'No installation found');
-      return res.end();
+      return next(createError(400, 'No installation found'));
     }
 
     let result: AuthorizeResult;
@@ -23,19 +27,18 @@ const getCommonMiddleware = (query:InstallationQuery<boolean>, installerService:
       result = await installerService.installer.authorize(query);
 
       if (result.botToken == null) {
-        res.writeHead(403, `The installation for the team(${query.teamId || query.enterpriseId}) has no botToken`);
-        return res.end();
+        return next(createError(403, `The installation for the team(${query.teamId || query.enterpriseId}) has no botToken`));
       }
     }
     catch (e) {
       logger.error(e.message);
 
-      res.writeHead(500, e.message);
-      return res.end();
+      return next(createError(500, e.message));
     }
 
     // set authorized data
     req.authorizeResult = result;
+    next();
   };
 };
 @Middleware()
@@ -50,7 +53,7 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
   @Inject()
   installerService: InstallerService;
 
-  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|Res> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res, @Next() next: Next): Promise<void|Res> {
     const { body } = req;
     const teamId = body.team_id;
     const enterpriseId = body.enterprise_id;
@@ -62,7 +65,7 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
     };
 
     const commonMiddleware = getCommonMiddleware(query, this.installerService, this.logger);
-    await commonMiddleware(req, res);
+    await commonMiddleware(req, res, next);
   }
 
 }
@@ -79,13 +82,11 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     @Inject()
     installerService: InstallerService;
 
-    async use(@Req() req: SlackOauthReq, @Res() res:Res): Promise<void|Res> {
+    async use(@Req() req: SlackOauthReq, @Res() res:Res, @Next() next: Next): Promise<void|Res> {
       const { body } = req;
 
       if (body.payload == null) {
-        // do nothing
-        this.logger.info('body does not have payload');
-        return;
+        return next(createError(400, 'The request has no payload.'));
       }
 
       const payload = JSON.parse(body.payload);
@@ -102,7 +103,7 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
       };
 
       const commonMiddleware = getCommonMiddleware(query, this.installerService, this.logger);
-      await commonMiddleware(req, res);
+      await commonMiddleware(req, res, next);
     }
 
 }
@@ -118,7 +119,7 @@ export class AuthorizeEventsMiddleware implements IMiddleware {
   @Inject()
   installerService: InstallerService;
 
-  async use(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|Res> {
+  async use(@Req() req: SlackOauthReq, @Res() res: Res, @Next() next: Next): Promise<void|Res> {
     const { body } = req;
     const teamId = body.team_id;
     const enterpriseId = body.enterprise_id;
@@ -130,7 +131,7 @@ export class AuthorizeEventsMiddleware implements IMiddleware {
     };
 
     const commonMiddleware = getCommonMiddleware(query, this.installerService, this.logger);
-    await commonMiddleware(req, res);
+    await commonMiddleware(req, res, next);
   }
 
 }

+ 5 - 3
packages/slackbot-proxy/src/middlewares/slack-to-growi/join-to-conversation.ts

@@ -23,9 +23,11 @@ export class JoinToConversationMiddleware implements IMiddleware {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const client = generateWebClient(authorizeResult.botToken!);
 
-    const joinResult = await client.conversations.join({ channel: body.channel_id });
-    if (!joinResult.ok) {
-      logger.error(joinResult.error, joinResult);
+    try {
+      await client.conversations.join({ channel: body.channel_id });
+    }
+    catch (err) {
+      logger.error(err);
     }
   }
 

+ 4 - 2
packages/ui/package.json

@@ -1,13 +1,15 @@
 {
   "name": "@growi/ui",
-  "version": "4.4.0-RC",
+  "version": "4.4.3-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [
     "growi"
   ],
   "module": "dist/esm/index.js",
-  "files": ["dist"],
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
     "lint:styles": "stylelint src/styles/scss/**/*.scss",

+ 42 - 71
yarn.lock

@@ -1393,11 +1393,6 @@
   resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-1.0.1.tgz#1ba7886c0474cc31707acd195f7b9073406b743d"
   integrity sha512-gQZ0a3JcrCi1g+00D9CIbo2uPc6lnykqAsVaCbew8jsrdyF0f0cBngYgFKcTxW2vliT5I3K4lwD4DhM6hXeOjg==
 
-"@kobalab/socket.io-session@^1.0.3":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@kobalab/socket.io-session/-/socket.io-session-1.0.3.tgz#87d55896bb48f57c57f26f0235bf53345a0a4615"
-  integrity sha512-pen2rqNuZUsR453EVM9owqDIbelFKa5gizyNM9hscphKrdPIYissNa9efddYSVBH24q7pknxS5kxbfSw/YYOMg==
-
 "@lerna/add@4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f"
@@ -2758,12 +2753,12 @@
   dependencies:
     "@types/express" "*"
 
-"@types/cookie@^0.4.0":
+"@types/cookie@^0.4.1":
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
   integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
 
-"@types/cors@^2.8.10":
+"@types/cors@^2.8.12":
   version "2.8.12"
   resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
   integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
@@ -2931,16 +2926,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
   integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
 
-"@types/node@>= 8":
+"@types/node@>= 8", "@types/node@>=10.0.0":
   version "16.7.10"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc"
   integrity sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==
 
-"@types/node@>=10.0.0":
-  version "16.4.13"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.13.tgz#7dfd9c14661edc65cccd43a29eb454174642370d"
-  integrity sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==
-
 "@types/node@>=12":
   version "14.14.37"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
@@ -6761,7 +6751,7 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   dependencies:
     ms "^2.1.1"
 
-debug@~4.3.1:
+debug@~4.3.1, debug@~4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
   integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -7379,10 +7369,10 @@ engine.io-client@~3.3.1:
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
 
-engine.io-client@~5.1.2:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.2.tgz#27108da9b39ae03262443d945caf2caa3655c4cb"
-  integrity sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==
+engine.io-client@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.2.0.tgz#ae38c79a4af16258c0300e6819c0ea8ecc1597cd"
+  integrity sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==
   dependencies:
     base64-arraybuffer "0.1.4"
     component-emitter "~1.3.0"
@@ -7392,6 +7382,7 @@ engine.io-client@~5.1.2:
     parseqs "0.0.6"
     parseuri "0.0.6"
     ws "~7.4.2"
+    xmlhttprequest-ssl "~2.0.0"
     yeast "0.1.2"
 
 engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
@@ -7405,9 +7396,9 @@ engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
     has-binary2 "~1.0.2"
 
 engine.io-parser@~4.0.0, engine.io-parser@~4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
-  integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.3.tgz#83d3a17acfd4226f19e721bb22a1ee8f7662d2f6"
+  integrity sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==
   dependencies:
     base64-arraybuffer "0.1.4"
 
@@ -7422,10 +7413,10 @@ engine.io@~3.2.0:
     engine.io-parser "~2.1.0"
     ws "~3.3.1"
 
-engine.io@~5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-5.1.1.tgz#a1f97e51ddf10cbd4db8b5ff4b165aad3760cdd3"
-  integrity sha512-aMWot7H5aC8L4/T8qMYbLdvKlZOdJTH54FxfdFunTGvhMx1BHkJOntWArsVfgAZVwAO9LC2sryPWRcEeUzCe5w==
+engine.io@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-5.2.0.tgz#554cdd0230d89de7b1a49a809d7ee5a129d36809"
+  integrity sha512-d1DexkQx87IFr1FLuV+0f5kAm1Hk1uOVijLOb+D1sDO2QMb7YjE02VHtZtxo7xIXMgcWLb+vl3HRT0rI9tr4jQ==
   dependencies:
     accepts "~1.3.4"
     base64id "2.0.0"
@@ -16278,7 +16269,7 @@ react-copy-to-clipboard@^5.0.1:
     copy-to-clipboard "^3"
     prop-types "^15.5.8"
 
-react-dom@^16.2.0, react-dom@^16.4.1:
+react-dom@^16.2.0, react-dom@^16.8.3:
   version "16.14.0"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
   integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
@@ -16288,15 +16279,6 @@ react-dom@^16.2.0, react-dom@^16.4.1:
     prop-types "^15.6.2"
     scheduler "^0.19.1"
 
-react-dom@^16.8.3:
-  version "16.8.3"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32"
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-    prop-types "^15.6.2"
-    scheduler "^0.13.3"
-
 react-dropzone@^11.2.4:
   version "11.2.4"
   resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.4.tgz#391a8d2e41a8a974340f83524d306540192e3313"
@@ -16543,7 +16525,7 @@ react-waypoint@^10.1.0:
     prop-types "^15.0.0"
     react-is "^17.0.1"
 
-react@^16.2.0, react@^16.4.1:
+react@^16.2.0, react@^16.8.3:
   version "16.14.0"
   resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
   integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
@@ -16552,15 +16534,6 @@ react@^16.2.0, react@^16.4.1:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
 
-react@^16.8.3:
-  version "16.8.3"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-    prop-types "^15.6.2"
-    scheduler "^0.13.3"
-
 reactstrap@^8.9.0:
   version "8.9.0"
   resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-8.9.0.tgz#bca4afa3f5cd18899ef9b33d877a141886d5abae"
@@ -17446,13 +17419,6 @@ saxes@^5.0.1:
   dependencies:
     xmlchars "^2.2.0"
 
-scheduler@^0.13.3:
-  version "0.13.3"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
-  dependencies:
-    loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-
 scheduler@^0.19.1:
   version "0.19.1"
   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
@@ -18053,10 +18019,10 @@ socket.io-adapter@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
 
-socket.io-adapter@~2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.1.tgz#a442720cb09a4823cfb81287dda1f9b52d4ccdb2"
-  integrity sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw==
+socket.io-adapter@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
+  integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
 
 socket.io-client@2.1.1:
   version "2.1.1"
@@ -18096,16 +18062,16 @@ socket.io-client@^2.0.4:
     socket.io-parser "~3.3.0"
     to-array "0.1.4"
 
-socket.io-client@^4.0.0:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.3.tgz#236daa642a9f229932e00b7221e843bf74232a62"
-  integrity sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==
+socket.io-client@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.2.0.tgz#195feed3de40283b1ae3f7d02cf91d3eb2c905c1"
+  integrity sha512-3GJ2KMh7inJUNAOjgf8NaKJZJa9uRyfryh2LrVJyKyxmzoXlfW9DeDNqylJn0ovOFt4e/kRLNWzMt/YqqEWYSA==
   dependencies:
     "@types/component-emitter" "^1.2.10"
     backo2 "~1.0.2"
     component-emitter "~1.3.0"
-    debug "~4.3.1"
-    engine.io-client "~5.1.2"
+    debug "~4.3.2"
+    engine.io-client "~5.2.0"
     parseuri "0.0.6"
     socket.io-parser "~4.0.4"
 
@@ -18145,19 +18111,19 @@ socket.io@2.1.1:
     socket.io-client "2.1.1"
     socket.io-parser "~3.2.0"
 
-socket.io@^4.0.0:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.1.3.tgz#d114328ef27ab31b889611792959c3fa6d502500"
-  integrity sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==
+socket.io@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.2.0.tgz#9e1c09d3ea647e24963a2e7ba8ea5c847778e2ed"
+  integrity sha512-sjlGfMmnaWvTRVxGRGWyhd9ctpg4APxWAxu85O/SxekkxHhfxmePWZbaYCkeX5QQX0z1YEnKOlNt6w82E4Nzug==
   dependencies:
-    "@types/cookie" "^0.4.0"
-    "@types/cors" "^2.8.10"
+    "@types/cookie" "^0.4.1"
+    "@types/cors" "^2.8.12"
     "@types/node" ">=10.0.0"
     accepts "~1.3.4"
     base64id "~2.0.0"
-    debug "~4.3.1"
-    engine.io "~5.1.1"
-    socket.io-adapter "~2.3.1"
+    debug "~4.3.2"
+    engine.io "~5.2.0"
+    socket.io-adapter "~2.3.2"
     socket.io-parser "~4.0.4"
 
 socks-proxy-agent@^5.0.0:
@@ -21078,6 +21044,11 @@ xmlhttprequest-ssl@~1.5.4:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz#04f560915724b389088715cc0ed7813e9677bf57"
 
+xmlhttprequest-ssl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
+  integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
 xpath@0.0.27:
   version "0.0.27"
   resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"

Some files were not shown because too many files changed in this diff