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

Merge branch 'master' into imprv/migrate-like-states-to-swr

Shun Miyazawa 4 лет назад
Родитель
Сommit
8e3b3a8978
49 измененных файлов с 498 добавлено и 769 удалено
  1. 1 0
      .stylelintrc.json
  2. 43 1
      CHANGELOG.md
  3. 1 1
      lerna.json
  4. 5 1
      package.json
  5. 1 0
      packages/app/.stylelintrc.json
  6. 41 36
      packages/app/docker/Dockerfile
  7. 5 6
      packages/app/docker/Dockerfile.dockerignore
  8. 2 2
      packages/app/docker/README.md
  9. 11 14
      packages/app/package.json
  10. 3 1
      packages/app/resource/locales/en_US/translation.json
  11. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  12. 1 0
      packages/app/resource/locales/zh_CN/translation.json
  13. 0 27
      packages/app/src/client/legacy/crowi.js
  14. 4 21
      packages/app/src/client/services/PageContainer.js
  15. 3 3
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  16. 0 85
      packages/app/src/components/BookmarkButton.jsx
  17. 83 0
      packages/app/src/components/BookmarkButtons.tsx
  18. 49 15
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  19. 2 2
      packages/app/src/components/Navbar/SubNavButtons.jsx
  20. 10 2
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  21. 1 3
      packages/app/src/components/SubscribeButton.tsx
  22. 7 0
      packages/app/src/interfaces/bookmarks.ts
  23. 0 15
      packages/app/src/server/models/user.js
  24. 8 2
      packages/app/src/server/routes/apiv3/bookmarks.js
  25. 4 4
      packages/app/src/server/routes/apiv3/forgot-password.js
  26. 2 2
      packages/app/src/server/routes/apiv3/personal-setting.js
  27. 2 2
      packages/app/src/server/routes/index.js
  28. 1 1
      packages/app/src/server/routes/page.js
  29. 7 1
      packages/app/src/server/service/config-loader.ts
  30. 16 1
      packages/app/src/server/service/page.js
  31. 15 5
      packages/app/src/server/service/passport.ts
  32. 2 3
      packages/app/src/server/service/search.ts
  33. 2 0
      packages/app/src/server/util/swigFunctions.js
  34. 1 1
      packages/app/src/server/views/widget/page_content.html
  35. 21 0
      packages/app/src/stores/bookmarks.tsx
  36. 2 2
      packages/app/src/stores/context.tsx
  37. 6 3
      packages/app/src/stores/page.tsx
  38. 1 1
      packages/app/src/styles/_sidebar.scss
  39. 1 1
      packages/codemirror-textlint/package.json
  40. 1 2
      packages/core/package.json
  41. 2 3
      packages/plugin-attachment-refs/package.json
  42. 2 2
      packages/plugin-lsx/package.json
  43. 1 3
      packages/plugin-pukiwiki-like-linker/package.json
  44. 1 1
      packages/slack/package.json
  45. 49 36
      packages/slackbot-proxy/docker/Dockerfile
  46. 5 6
      packages/slackbot-proxy/docker/Dockerfile.dockerignore
  47. 2 2
      packages/slackbot-proxy/package.json
  48. 1 2
      packages/ui/package.json
  49. 69 448
      yarn.lock

+ 1 - 0
.stylelintrc.json

@@ -2,6 +2,7 @@
   "extends": [
     "stylelint-config-recess-order"
   ],
+  "customSyntax": "postcss-scss",
   "rules": {
     "indentation": 2,
     "string-quotes": "single",

+ 43 - 1
CHANGELOG.md

@@ -1,9 +1,51 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.5.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.5.8...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v4.5.8](https://github.com/weseek/growi/compare/v4.5.7...v4.5.8) - 2022-01-12
+
+### 💎 Features
+
+- feat: Display a list of bookmarked users (#5044) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Built-in editor scroll position is reset after save (Introduced by v4.5.3) (#5074) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Bump y18n to v4.0.3 (#5071) @yuki-takei
+- support: Omit prettier-stylelint (#5070) @yuki-takei
+- support: Bump tar to 6.1.11 (#5069) @yuki-takei
+
+## [v4.5.7](https://github.com/weseek/growi/compare/v4.5.6...v4.5.7) - 2022-01-11
+
+### 🐛 Bug Fixes
+
+- fix: Subnavigation sticking initialization (#5062) @yuki-takei
+- fix: Built-in editor was broken (#5061) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Bump re2 to 1.17.2 (#5059) @yuki-takei
+
+## [v4.5.6](https://github.com/weseek/growi/compare/v4.5.5...v4.5.6) - 2022-01-07
+
+### 💎 Features
+
+- feat: Resolve conflict with 3-way merge like editor (#5012) @yuto-oweseek
+
+### 🚀 Improvement
+
+- imprv: Subnavigation behavior (#5047) @yuki-takei
+- imprv: Switching editor mode behavior (#5043) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- Bug: Error: The specified instance couldn't register because same id has already been registered (#5031) by #5043 @yuki-takei
+
 ## [v4.5.5](https://github.com/weseek/growi/compare/v4.5.4...v4.5.5) - 2022-01-05
 
 ### 💎 Features

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "packages": [
     "packages/*"
   ]

+ 5 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -67,8 +67,12 @@
     "jest-date-mock": "^1.0.8",
     "jest-localstorage-mock": "^2.4.14",
     "lerna": "^4.0.0",
+    "postcss": "^8.4.5",
+    "postcss-scss": "^4.0.3",
     "rewire": "^5.0.0",
     "shipjs": "^0.24.1",
+    "stylelint": "^14.2.0",
+    "stylelint-config-recess-order": "^3.0.0",
     "ts-jest": "^27.0.4",
     "ts-node": "^9.1.1",
     "tsconfig-paths": "^3.9.0",

+ 1 - 0
packages/app/.stylelintrc.json

@@ -2,6 +2,7 @@
   "extends": [
     "stylelint-config-recess-order"
   ],
+  "customSyntax": "postcss-scss",
   "ignoreFiles": [
     "src/styles/_override-bootstrap-variables.scss",
     "src/linter-checker/test.scss"

+ 41 - 36
packages/app/docker/Dockerfile

@@ -1,36 +1,40 @@
-# syntax = docker/dockerfile:experimental
+# syntax = docker/dockerfile:1
 
 ARG flavor=default
 
 
+##
+## packages-json-picker
+##
+FROM node:14-slim AS packages-json-picker
+
+ENV optDir /opt
+
+WORKDIR ${optDir}
+COPY ["package.json", "yarn.lock", "lerna.json", "./"]
+COPY packages packages
+# Find and remove non-package.json files
+RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
+
 
 ##
 ## deps-resolver
 ##
 FROM node:14-slim AS deps-resolver
-LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
-ENV appDir /opt/growi
+ENV optDir /opt
 
-WORKDIR ${appDir}
-COPY ./package.json .
-COPY ./yarn.lock .
-COPY ./lerna.json .
-COPY ./packages/app/package.json packages/app/
-COPY ./packages/core/package.json packages/core/
-COPY ./packages/codemirror-textlint/package.json packages/codemirror-textlint/
-COPY ./packages/plugin-attachment-refs/package.json packages/plugin-attachment-refs/
-COPY ./packages/plugin-lsx/package.json packages/plugin-lsx/
-COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
-COPY ./packages/slack/package.json packages/slack/
-COPY ./packages/ui/package.json packages/ui/
+WORKDIR ${optDir}
+
+# copy files
+COPY --from=packages-json-picker ${optDir} .
 
 # setup
 RUN yarn config set network-timeout 300000
-RUN npx lerna bootstrap
+RUN npx lerna bootstrap -- --frozen-lockfile
 
 # make artifacts
-RUN tar cf node_modules.tar \
+RUN tar -cf node_modules.tar \
   node_modules \
   packages/*/node_modules
 
@@ -40,9 +44,13 @@ RUN tar cf node_modules.tar \
 ## deps-resolver-prod
 ##
 FROM deps-resolver AS deps-resolver-prod
+
+# remove unnecessary packages
+RUN rm -rf packages/slackbot-proxy
+
 RUN npx lerna bootstrap -- --production
 # make artifacts
-RUN tar cf node_modules.tar \
+RUN tar -cf node_modules.tar \
   node_modules \
   packages/*/node_modules
 
@@ -53,16 +61,16 @@ RUN tar cf node_modules.tar \
 ##
 FROM node:14-slim AS prebuilder-default
 
-ENV appDir /opt/growi
+ENV optDir /opt
 
-WORKDIR ${appDir}
+WORKDIR ${optDir}
 
 # copy dependent packages
 COPY --from=deps-resolver \
-  ${appDir}/node_modules.tar ${appDir}/
+  ${optDir}/node_modules.tar ${optDir}/
 
 # extract node_modules.tar
-RUN tar xf node_modules.tar
+RUN tar -xf node_modules.tar
 RUN rm node_modules.tar
 
 
@@ -73,7 +81,7 @@ RUN rm node_modules.tar
 FROM prebuilder-default AS prebuilder-nocdn
 
 # add dotenv file for NO_CDN
-COPY packages/app/docker/nocdn/.env.production.local ${appDir}/packages/app/
+COPY packages/app/docker/nocdn/.env.production.local ${optDir}/packages/app/
 
 
 
@@ -82,14 +90,11 @@ COPY packages/app/docker/nocdn/.env.production.local ${appDir}/packages/app/
 ##
 FROM prebuilder-${flavor} AS builder
 
-ENV appDir /opt/growi
+ENV optDir /opt
 
-WORKDIR ${appDir}
+WORKDIR ${optDir}
 
-COPY ./package.json ./
-COPY ./yarn.lock ./
-COPY ./lerna.json ./
-COPY ./tsconfig.base.json ./
+COPY ["package.json", "lerna.json", "tsconfig.base.json", "./"]
 # copy all related packages
 COPY packages/app packages/app
 COPY packages/core packages/core
@@ -104,9 +109,8 @@ COPY packages/ui packages/ui
 RUN yarn lerna run build
 
 # make artifacts
-RUN tar cf packages.tar \
+RUN tar -cf packages.tar \
   package.json \
-  yarn.lock \
   tsconfig.base.json \
   packages/app/config \
   packages/app/public \
@@ -129,7 +133,8 @@ LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 ENV NODE_ENV production
 
-ENV appDir /opt/growi
+ENV optDir /opt
+ENV appDir ${optDir}/growi
 
 # Add gosu
 # see: https://github.com/tianon/gosu/blob/1.13/INSTALL.md
@@ -141,15 +146,15 @@ RUN set -eux; \
 	gosu nobody true
 
 COPY --from=deps-resolver-prod --chown=node:node \
-  ${appDir}/node_modules.tar ${appDir}/
+  ${optDir}/node_modules.tar ${appDir}/
 COPY --from=builder --chown=node:node \
-  ${appDir}/packages.tar ${appDir}/
+  ${optDir}/packages.tar ${appDir}/
 
 # extract artifacts as 'node' user
 USER node
 WORKDIR ${appDir}
-RUN tar xf node_modules.tar
-RUN tar xf packages.tar
+RUN tar -xf node_modules.tar
+RUN tar -xf packages.tar
 RUN rm node_modules.tar packages.tar
 
 USER root

+ 5 - 6
packages/app/docker/Dockerfile.dockerignore

@@ -1,6 +1,5 @@
-node_modules
-*/node_modules
-*/coverage
-*/dist
-*/Dockerfile
-*/*.dockerignore
+**/node_modules
+**/dist
+**/coverage
+**/Dockerfile
+**/*.dockerignore

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.5.5`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.5/docker/Dockerfile)
-* [`4.5.5-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.5/docker/Dockerfile)
+* [`4.5.8`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.8/docker/Dockerfile)
+* [`4.5.8-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.8/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 11 - 14
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -33,7 +33,7 @@
     "predev:ci": "run-p resources:*",
     "lint:typecheck": "npx tsc",
     "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
-    "lint:styles": "stylelint src/**/*.scss --custom-syntax postcss-scss",
+    "lint:styles": "stylelint src/**/*.scss",
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
@@ -57,11 +57,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.5.6-RC.0",
-    "@growi/plugin-attachment-refs": "^4.5.6-RC.0",
-    "@growi/plugin-lsx": "^4.5.6-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.5.6-RC.0",
-    "@growi/slack": "^4.5.6-RC.0",
+    "@growi/codemirror-textlint": "^4.5.9-RC.0",
+    "@growi/plugin-attachment-refs": "^4.5.9-RC.0",
+    "@growi/plugin-lsx": "^4.5.9-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.5.9-RC.0",
+    "@growi/slack": "^4.5.9-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -73,6 +73,7 @@
     "async-canvas-to-blob": "^1.0.3",
     "aws-sdk": "^2.1044.0",
     "axios": "^0.24.0",
+    "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
@@ -99,7 +100,6 @@
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
-    "got": "^8.3.2",
     "graceful-fs": "^4.1.11",
     "helmet": "^4.6.0",
     "http-errors": "~1.8.0",
@@ -123,6 +123,7 @@
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
+    "p-retry": "^4.0.0",
     "passport": "^0.5.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
@@ -131,9 +132,7 @@
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-twitter": "^1.0.4",
-    "p-retry": "^4.0.0",
     "prom-client": "^13.0.0",
-    "re2": "^1.17.1",
     "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "react-tagcloud": "^2.1.1",
@@ -160,7 +159,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^4.5.6-RC.0",
+    "@growi/ui": "^4.5.9-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -233,10 +232,8 @@
     "sticky-events": "^3.4.11",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
-    "stylelint": "^14.0.1",
-    "stylelint-config-recess-order": "^2.0.1",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.1.0",
+    "swr": "^1.1.2",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",

+ 3 - 1
packages/app/resource/locales/en_US/translation.json

@@ -58,6 +58,8 @@
   "The end": "The end",
   "Not available for guest": "Not available for guest",
   "No users have liked this yet": "No users have liked this yet",
+  "No users have liked this yet.": "No users have liked this yet.",
+  "No users have bookmarked yet": "No users have bookmarked yet",
   "Create Archive Page": "Create Archive Page",
   "File type": "File type",
   "Target page": "Target page",
@@ -193,7 +195,7 @@
     },
     "form_help": {
       "email": "You must have email address which listed below to sign up to this wiki.",
-      "password": "Your password must be at least 6 characters long.",
+      "password": "Your password must be at least 8 characters long.",
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
     }
   },

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -59,6 +59,7 @@
   "The end": "おしまい",
   "Not available for guest": "ゲストユーザーは利用できません",
   "No users have liked this yet": "いいねをしているユーザーはいません",
+  "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "Create Archive Page": "アーカイブページの作成",
   "Target page": "対象ページ",
   "File type": "ファイル形式",

+ 1 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -60,6 +60,7 @@
   "The end": "结束",
   "Not available for guest": "Not available for guest",
   "No users have liked this yet": "还没有用户喜欢这个",
+  "No users have bookmarked yet": "还没有用户加入书签",
   "Create Archive Page": "创建归档页",
   "File type": "文件类型",
   "Target page": "目标页面",

+ 0 - 27
packages/app/src/client/legacy/crowi.js

@@ -112,33 +112,6 @@ Crowi.initClassesByOS = function() {
   });
 };
 
-// window.addEventListener('load', () => {
-//   const { appContainer } = window;
-//   const pageContainer = appContainer.getContainer('PageContainer');
-
-//   // Do nothing if the page does not exist
-//   // ex.) admin page,login page
-//   if (pageContainer == null) {
-//     return null;
-//   }
-//   const { isAbleToOpenPageEditor } = pageContainer;
-
-//   // hash on page
-//   if (window.location.hash) {
-//     const navigationContainer = appContainer.getContainer('NavigationContainer');
-
-//     if (window.location.hash === '#edit' && isAbleToOpenPageEditor) {
-//       navigationContainer.setEditorMode('edit');
-
-//       // focus
-//       Crowi.setCaretLineAndFocusToEditor();
-//     }
-//     else if (window.location.hash === '#hackmd') {
-//       navigationContainer.setEditorMode('hackmd');
-//     }
-//   }
-// });
-
 window.addEventListener('load', () => {
   const crowi = window.crowi;
   if (crowi && crowi.users && crowi.users.length !== 0) {

+ 4 - 21
packages/app/src/client/services/PageContainer.js

@@ -5,10 +5,10 @@ import * as entities from 'entities';
 import * as toastr from 'toastr';
 import { pagePathUtils } from '@growi/core';
 
-import { apiPost } from '../util/apiv1-client';
 import loggerFactory from '~/utils/logger';
-import { toastError } from '../util/apiNotification';
+import { EditorMode } from '~/stores/ui';
 
+import { toastError } from '../util/apiNotification';
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
@@ -54,9 +54,6 @@ export default class PageContainer extends Container {
       path,
       tocHtml: '',
 
-      isBookmarked: false,
-      sumOfBookmarks: 0,
-
       createdAt: mainContent.getAttribute('data-page-created-at'),
       // please use useCurrentUpdatedAt instead
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
@@ -246,20 +243,6 @@ export default class PageContainer extends Container {
     this.state.markdown = markdown;
   }
 
-  async retrieveBookmarkInfo() {
-    const response = await this.appContainer.apiv3Get('/bookmarks/info', { pageId: this.state.pageId });
-    this.setState({
-      sumOfBookmarks: response.data.sumOfBookmarks,
-      isBookmarked: response.data.isBookmarked,
-    });
-  }
-
-  async toggleBookmark() {
-    const bool = !this.state.isBookmarked;
-    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
-    return this.retrieveBookmarkInfo();
-  }
-
   setLatestRemotePageData(s2cMessagePageUpdated) {
     const newState = {
       remoteRevisionId: s2cMessagePageUpdated.revisionId,
@@ -312,7 +295,7 @@ export default class PageContainer extends Container {
     // PageEditor component
     const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     if (pageEditor != null) {
-      if (editorMode !== 'edit') {
+      if (editorMode !== EditorMode.Editor) {
         pageEditor.updateEditorValue(newState.markdown);
       }
     }
@@ -320,7 +303,7 @@ export default class PageContainer extends Container {
     const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
     if (pageEditorByHackmd != null) {
       // reset
-      if (editorMode !== 'hackmd') {
+      if (editorMode !== EditorMode.HackMD) {
         pageEditorByHackmd.reset();
       }
     }

+ 3 - 3
packages/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -157,10 +157,10 @@ class SecuritySetting extends React.Component {
                 aria-expanded="true"
               >
                 <span className="float-left">
-                  {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
+                  {(currentPageCompleteDeletionAuthority === 'anyOne' || currentPageCompleteDeletionAuthority == null)
+                      && t('security_setting.anyone')}
                   {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
-                  {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
-                      && t('security_setting.admin_and_author')}
+                  {currentPageCompleteDeletionAuthority === 'adminAndAuthor' && t('security_setting.admin_and_author')}
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">

+ 0 - 85
packages/app/src/components/BookmarkButton.jsx

@@ -1,85 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UncontrolledTooltip } from 'reactstrap';
-import { withTranslation } from 'react-i18next';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import { toastError } from '~/client/util/apiNotification';
-import PageContainer from '~/client/services/PageContainer';
-import AppContainer from '~/client/services/AppContainer';
-
-class BookmarkButton extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.handleClick = this.handleClick.bind(this);
-  }
-
-  async handleClick() {
-    const { appContainer, pageContainer } = this.props;
-    const { isGuestUser } = appContainer;
-
-    if (isGuestUser) {
-      return;
-    }
-
-    try {
-      pageContainer.toggleBookmark();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-
-  render() {
-    const { appContainer, pageContainer, t } = this.props;
-    const { isGuestUser } = appContainer;
-
-    return (
-      <div>
-        <button
-          type="button"
-          id="bookmark-button"
-          onClick={this.handleClick}
-          className={`btn btn-bookmark border-0
-          ${`btn-${this.props.size}`} ${pageContainer.state.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
-        >
-          <i className="icon-star mr-3"></i>
-          <span className="total-bookmarks">
-            {pageContainer.state.sumOfBookmarks}
-          </span>
-        </button>
-
-        {isGuestUser && (
-          <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
-            {t('Not available for guest')}
-          </UncontrolledTooltip>
-        )}
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer, PageContainer]);
-
-BookmarkButton.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
-  pageId: PropTypes.string,
-  t: PropTypes.func.isRequired,
-  size: PropTypes.string,
-};
-
-BookmarkButton.defaultProps = {
-  size: 'md',
-};
-
-export default withTranslation()(BookmarkButtonWrapper);

+ 83 - 0
packages/app/src/components/BookmarkButtons.tsx

@@ -0,0 +1,83 @@
+import React, { FC, useState } from 'react';
+
+import { Types } from 'mongoose';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+
+import UserPictureList from './User/UserPictureList';
+import { toastError } from '~/client/util/apiNotification';
+import { useIsGuestUser } from '~/stores/context';
+import { useSWRxBookmarksInfo } from '~/stores/bookmarks';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
+interface Props {
+  pageId: Types.ObjectId
+}
+
+const BookmarkButton: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+  const { pageId } = props;
+
+  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: bookmarksInfo, mutate } = useSWRxBookmarksInfo(pageId);
+
+  const isBookmarked = bookmarksInfo?.isBookmarked != null ? bookmarksInfo.isBookmarked : false;
+  const sumOfBookmarks = bookmarksInfo?.sumOfBookmarks != null ? bookmarksInfo.sumOfBookmarks : 0;
+  const bookmarkedUsers = bookmarksInfo?.bookmarkedUsers != null ? bookmarksInfo.bookmarkedUsers : [];
+
+  const togglePopover = () => {
+    setIsPopoverOpen(!isPopoverOpen);
+  };
+
+  const handleClick = async() => {
+    if (isGuestUser) {
+      return;
+    }
+
+    try {
+      const res = await apiv3Put('/bookmarks', { pageId, bool: !isBookmarked });
+      if (res) {
+        mutate();
+      }
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <div className="btn-group" role="group" aria-label="Bookmark buttons">
+      <button
+        type="button"
+        id="bookmark-button"
+        onClick={handleClick}
+        className={`btn btn-bookmark border-0
+          ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+      >
+        <i className="icon-star"></i>
+      </button>
+
+      {isGuestUser && (
+        <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      )}
+
+      <button type="button" id="po-total-bookmarks" className={`btn btn-bookmark border-0 total-bookmarks ${isBookmarked ? 'active' : ''}`}>
+        {sumOfBookmarks}
+      </button>
+
+      <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
+        <PopoverBody className="seen-user-popover">
+          <div className="px-2 text-right user-list-content text-truncate text-muted">
+            {bookmarkedUsers.length ? <UserPictureList users={bookmarkedUsers} /> : t('No users have bookmarked yet')}
+          </div>
+        </PopoverBody>
+      </Popover>
+    </div>
+  );
+};
+
+export default BookmarkButton;

+ 49 - 15
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -1,9 +1,12 @@
-import React, { useState, useEffect, useCallback } from 'react';
-// import PropTypes from 'prop-types';
+import React, {
+  useMemo, useState, useRef, useEffect, useCallback,
+} from 'react';
 
 import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
+
 import loggerFactory from '~/utils/logger';
+import { useSidebarCollapsed } from '~/stores/ui';
 
 import GrowiSubNavigation from './GrowiSubNavigation';
 
@@ -21,24 +24,43 @@ const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
  */
 const GrowiSubNavigationSwitcher = (props) => {
 
+  const { data: isSidebarCollapsed } = useSidebarCollapsed();
+
   const [isVisible, setVisible] = useState(false);
+  const [width, setWidth] = useState(null);
+
+  const fixedContainerRef = useRef();
+  const stickyEvents = useMemo(() => new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' }), []);
 
-  const resetWidth = useCallback(() => {
-    const elem = document.getElementById('grw-subnav-fixed-container');
+  const initWidth = useCallback(() => {
+    const instance = fixedContainerRef.current;
 
-    if (elem == null || elem.parentNode == null) {
+    if (instance == null || instance.parentNode == null) {
       return;
     }
 
     // get parent width
-    const { clientWidth: width } = elem.parentNode;
+    const { clientWidth } = instance.parentNode;
     // update style
-    elem.style.width = `${width}px`;
+    setWidth(clientWidth);
   }, []);
 
+  const initVisible = useCallback(() => {
+    const elements = stickyEvents.stickyElements;
+
+    for (const elem of elements) {
+      const bool = stickyEvents.isSticking(elem);
+      if (bool) {
+        setVisible(bool);
+        break;
+      }
+    }
+
+  }, [stickyEvents]);
+
   // setup effect by resizing event
   useEffect(() => {
-    const resizeHandler = debounce(100, resetWidth);
+    const resizeHandler = debounce(100, initWidth);
 
     window.addEventListener('resize', resizeHandler);
 
@@ -46,7 +68,7 @@ const GrowiSubNavigationSwitcher = (props) => {
     return () => {
       window.removeEventListener('resize', resizeHandler);
     };
-  }, [resetWidth]);
+  }, [initWidth]);
 
   const stickyChangeHandler = useCallback((event) => {
     logger.debug('StickyEvents.CHANGE detected');
@@ -57,7 +79,6 @@ const GrowiSubNavigationSwitcher = (props) => {
   useEffect(() => {
     // sticky
     // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' });
     const { stickySelector } = stickyEvents;
     const elem = document.querySelector(stickySelector);
     elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
@@ -66,16 +87,29 @@ const GrowiSubNavigationSwitcher = (props) => {
     return () => {
       elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
     };
-  }, [stickyChangeHandler]);
+  }, [stickyChangeHandler, stickyEvents]);
+
+  // update width when sidebar collapsing changed
+  useEffect(() => {
+    if (isSidebarCollapsed != null) {
+      setTimeout(initWidth, 300);
+    }
+  }, [isSidebarCollapsed, initWidth]);
 
-  // update width
+  // initialize
   useEffect(() => {
-    resetWidth();
-  });
+    initWidth();
+
+    // check sticky state several times
+    setTimeout(initVisible, 100);
+    setTimeout(initVisible, 300);
+    setTimeout(initVisible, 2000);
+
+  }, [initWidth, initVisible]);
 
   return (
     <div className={`grw-subnav-switcher ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
-      <div id="grw-subnav-fixed-container" className="grw-subnav-fixed-container position-fixed">
+      <div id="grw-subnav-fixed-container" className="grw-subnav-fixed-container position-fixed" ref={fixedContainerRef} style={{ width }}>
         <GrowiSubNavigation isCompactMode />
       </div>
     </div>

+ 2 - 2
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -6,7 +6,7 @@ import { usePageId } from '~/stores/context';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import BookmarkButton from '../BookmarkButton';
+import BookmarkButtons from '../BookmarkButtons';
 import LikeButtons from '../LikeButtons';
 import SubscribeButton from '../SubscribeButton';
 import PageManagement from '../Page/PageManagement';
@@ -33,7 +33,7 @@ const SubnavButtons = React.memo((props) => {
           </span>
         )}
         <span>
-          <BookmarkButton />
+          <BookmarkButtons pageId={pageId} />
         </span>
       </>
     );

+ 10 - 2
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 
 import urljoin from 'url-join';
 import * as codemirror from 'codemirror';
+import { UnControlled as UncontrolledCodeMirror } from 'react-codemirror2';
 
 import { Button } from 'reactstrap';
 
@@ -31,7 +32,7 @@ import LinkEditModal from './LinkEditModal';
 import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
+// import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 // Textlint
 window.JSHINT = JSHINT;
@@ -109,7 +110,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.state = {
       value: this.props.value,
-      isGfmMode: this.props.isGfmMode,
+      isGfmMode: this.props.isGfmMode ?? true,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
@@ -908,6 +909,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   render() {
+    const mode = this.state.isGfmMode ? 'gfm-growi' : undefined;
     const lint = this.props.isTextlintEnabled ? this.codemirrorLintConfig : false;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
@@ -934,6 +936,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
           }}
           value={this.state.value}
           options={{
+            mode,
+            theme: this.props.editorOptions.theme,
+            styleActiveLine: this.props.editorOptions.styleActiveLine,
+            lineNumbers: this.props.lineNumbers,
+            tabSize: 4,
+            indentUnit: this.props.indentSize,
             lineWrapping: true,
             scrollPastEnd: true,
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei

+ 1 - 3
packages/app/src/components/SubscribeButton.tsx

@@ -1,7 +1,5 @@
 import React, { FC } from 'react';
 
-
-import { Types } from 'mongoose';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { useSWRxSubscriptionStatus } from '../stores/page';
@@ -12,7 +10,7 @@ import { apiv3Put } from '~/client/util/apiv3-client';
 import { useIsGuestUser } from '~/stores/context';
 
 type Props = {
-  pageId: Types.ObjectId,
+  pageId: string,
 };
 
 const SubscribeButton: FC<Props> = (props: Props) => {

+ 7 - 0
packages/app/src/interfaces/bookmarks.ts

@@ -0,0 +1,7 @@
+import { IUser } from '~/interfaces/user';
+
+export interface IBookmarksInfo {
+  isBookmarked: boolean
+  sumOfBookmarks: number
+  bookmarkedUsers: IUser[]
+}

+ 0 - 15
packages/app/src/server/models/user.js

@@ -187,21 +187,6 @@ module.exports = function(crowi) {
     return userData;
   };
 
-  userSchema.methods.canDeleteCompletely = function(creatorId) {
-    const pageCompleteDeletionAuthority = crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
-    if (this.admin) {
-      return true;
-    }
-    if (pageCompleteDeletionAuthority === 'anyOne' || pageCompleteDeletionAuthority == null) {
-      return true;
-    }
-    if (pageCompleteDeletionAuthority === 'adminAndAuthor') {
-      return (this._id.equals(creatorId));
-    }
-
-    return false;
-  };
-
   userSchema.methods.updateApiToken = async function() {
     const self = this;
 

+ 8 - 2
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -113,10 +113,16 @@ module.exports = (crowi) => {
     const responsesParams = {};
 
     try {
-      responsesParams.sumOfBookmarks = await Bookmark.countByPageId(pageId);
+      const bookmarks = await Bookmark.find({ page: pageId }).populate('user');
+      let users = [];
+      if (bookmarks.length > 0) {
+        users = bookmarks.map(bookmark => serializeUserSecurely(bookmark.user));
+      }
+      responsesParams.sumOfBookmarks = bookmarks.length;
+      responsesParams.bookmarkedUsers = users;
     }
     catch (err) {
-      logger.error('get-bookmark-count-failed', err);
+      logger.error('get-bookmark-document-failed', err);
       return res.apiv3Err(err, 500);
     }
 

+ 4 - 4
packages/app/src/server/routes/apiv3/forgot-password.js

@@ -23,8 +23,8 @@ module.exports = (crowi) => {
   const validator = {
     password: [
       body('newPassword').isString().not().isEmpty()
-        .isLength({ min: 6 })
-        .withMessage('password must be at least 6 characters long'),
+        .isLength({ min: 8 })
+        .withMessage('password must be at least 8 characters long'),
       // checking if password confirmation matches password
       body('newPasswordConfirm').isString().not().isEmpty()
         .custom((value, { req }) => {
@@ -35,7 +35,7 @@ module.exports = (crowi) => {
 
   const apiLimiter = rateLimit({
     windowMs: 15 * 60 * 1000, // 15 minutes
-    max: 5, // limit each IP to 5 requests per windowMs
+    max: 10, // limit each IP to 10 requests per windowMs
     message:
       'Too many requests were sent from this IP. Please try a password reset request again on the password reset request form',
   });
@@ -81,7 +81,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.put('/', injectResetOrderByTokenMiddleware, async(req, res) => {
+  router.put('/', apiLimiter, injectResetOrderByTokenMiddleware, csrf, validator.password, apiV3FormValidator, async(req, res) => {
     const { passwordResetOrder } = req;
     const { email } = passwordResetOrder;
     const grobalLang = configManager.getConfig('crowi', 'app:globalLang');

+ 2 - 2
packages/app/src/server/routes/apiv3/personal-setting.js

@@ -86,8 +86,8 @@ module.exports = (crowi) => {
     password: [
       body('oldPassword').isString(),
       body('newPassword').isString().not().isEmpty()
-        .isLength({ min: 6 })
-        .withMessage('password must be at least 6 characters long'),
+        .isLength({ min: 8 })
+        .withMessage('password must be at least 8 characters long'),
       body('newPasswordConfirm').isString().not().isEmpty()
         .custom((value, { req }) => {
           return (value === req.body.newPassword);

+ 2 - 2
packages/app/src/server/routes/index.js

@@ -13,7 +13,7 @@ const rateLimit = require('express-rate-limit');
 
 const apiLimiter = rateLimit({
   windowMs: 15 * 60 * 1000, // 15 minutes
-  max: 5, // limit each IP to 5 requests per windowMs
+  max: 10, // limit each IP to 5 requests per windowMs
   message:
     'Too many requests sent from this IP, please try again after 15 minutes',
 });
@@ -61,7 +61,7 @@ module.exports = function(crowi, app) {
   app.get('/login'                    , applicationInstalled, login.preLogin, login.login);
   app.get('/login/invited'            , applicationInstalled, login.invited);
   app.post('/login/activateInvited'   , applicationInstalled, form.invited                         , csrf, login.invited);
-  app.post('/login'                   , applicationInstalled, form.login                           , csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
+  app.post('/login'                   , apiLimiter, applicationInstalled, form.login                           , csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   app.post('/register'                , applicationInstalled, form.register                        , csrf, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);

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

@@ -1094,7 +1094,7 @@ module.exports = function(crowi, app) {
 
     try {
       if (isCompletely) {
-        if (!req.user.canDeleteCompletely(page.creator)) {
+        if (!crowi.pageService.canDeleteCompletely(page.creator, req.user)) {
           return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
         }
         await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively);

+ 7 - 1
packages/app/src/server/service/config-loader.ts

@@ -407,7 +407,13 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     ns: 'crowi',
     key: 'security:passport-oidc:oidcClientClockTolerance',
     type: ValueType.NUMBER,
-    default: 10,
+    default: 60,
+  },
+  OIDC_ISSUER_TIMEOUT_OPTION: {
+    ns: 'crowi',
+    key: 'security:passport-oidc:oidcIssuerTimeoutOption',
+    type: ValueType.NUMBER,
+    default: 5000,
   },
   S3_REFERENCE_FILE_WITH_RELAY_MODE: {
     ns:      'crowi',

+ 16 - 1
packages/app/src/server/service/page.js

@@ -100,6 +100,21 @@ class PageService {
     });
   }
 
+  canDeleteCompletely(creatorId, operator) {
+    const pageCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
+    if (operator.admin) {
+      return true;
+    }
+    if (pageCompleteDeletionAuthority === 'anyOne' || pageCompleteDeletionAuthority == null) {
+      return true;
+    }
+    if (pageCompleteDeletionAuthority === 'adminAndAuthor') {
+      return (operator._id.equals(creatorId));
+    }
+
+    return false;
+  }
+
   async findPageAndMetaDataByViewer({ pageId, path, user }) {
 
     const Page = this.crowi.model('Page');
@@ -132,7 +147,7 @@ class PageService {
     result.isCreatable = false;
     result.isDeletable = isDeletablePage(path);
     result.isDeleted = page.isDeleted();
-    result.canDeleteCompletely = user != null && user.canDeleteCompletely(page.creator);
+    result.canDeleteCompletely = user != null && this.canDeleteCompletely(page.creator, user);
 
     return result;
   }

+ 15 - 5
packages/app/src/server/service/passport.ts

@@ -12,7 +12,7 @@ import { Profile, Strategy as SamlStrategy, VerifiedCallback } from 'passport-sa
 import { BasicStrategy } from 'passport-http';
 
 import { IncomingMessage } from 'http';
-import got from 'got';
+import axiosRetry from 'axios-retry';
 import pRetry from 'p-retry';
 import loggerFactory from '~/utils/logger';
 
@@ -622,7 +622,8 @@ class PassportService implements S2sMessageHandlable {
 
     // setup client
     // extend oidc request timeouts
-    OIDCIssuer.defaultHttpOptions = { timeout: 5000 };
+    const OIDC_ISSUER_TIMEOUT_OPTION = await this.crowi.configManager.getConfig('crowi', 'security:passport-oidc:oidcIssuerTimeoutOption');
+    OIDCIssuer.defaultHttpOptions = { timeout: OIDC_ISSUER_TIMEOUT_OPTION };
     const issuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
     const clientId = configManager.getConfig('crowi', 'security:passport-oidc:clientId');
     const clientSecret = configManager.getConfig('crowi', 'security:passport-oidc:clientSecret');
@@ -720,8 +721,17 @@ class PassportService implements S2sMessageHandlable {
  */
   async isOidcHostReachable(issuerHost) {
     try {
-      const response = await got(issuerHost, { retry: { limit: 3 } });
-      return response.statusCode === 200;
+      const client = require('axios').default;
+      axiosRetry(client, {
+        retries: 3,
+      });
+      const response = await client.get(`${issuerHost}/.well-known/openid-configuration`);
+      // Check for valid OIDC Issuer configuration
+      if (!response.data.issuer) {
+        logger.debug('OidcStrategy: Invalid OIDC Issuer configurations');
+        return false;
+      }
+      return true;
     }
     catch (err) {
       logger.error('OidcStrategy: issuer host unreachable:', err.code);
@@ -740,7 +750,7 @@ class PassportService implements S2sMessageHandlable {
     const OIDC_DISCOVERY_RETRIES = await this.crowi.configManager.getConfig('crowi', 'security:passport-oidc:discoveryRetries');
     const oidcIssuerHostReady = await this.isOidcHostReachable(issuerHost);
     if (!oidcIssuerHostReady) {
-      logger.error('OidcStrategy: setup failed: OIDC Issur host unreachable');
+      logger.error('OidcStrategy: setup failed');
       return;
     }
     const oidcIssuer = await pRetry(async() => {

+ 2 - 3
packages/app/src/server/service/search.ts

@@ -1,4 +1,3 @@
-import RE2 from 're2';
 import xss from 'xss';
 
 import { SearchDelegatorName } from '~/interfaces/named-query';
@@ -198,8 +197,8 @@ class SearchService implements SearchQueryParser, SearchResolver {
   }
 
   async parseSearchQuery(_queryString: string): Promise<ParsedQuery> {
-    const regexp = new RE2(/^\[nq:.+\]$/g); // https://regex101.com/r/FzDUvT/1
-    const replaceRegexp = new RE2(/\[nq:|\]/g);
+    const regexp = new RegExp(/^\[nq:.+\]$/g); // https://regex101.com/r/FzDUvT/1
+    const replaceRegexp = new RegExp(/\[nq:|\]/g);
 
     const queryString = normalizeQueryString(_queryString);
 

+ 2 - 0
packages/app/src/server/util/swigFunctions.js

@@ -13,6 +13,7 @@ module.exports = function(crowi, req, locals) {
     appService,
     aclService,
     customizeService,
+    pageService,
   } = crowi;
   debug('initializing swigFunctions');
 
@@ -70,6 +71,7 @@ module.exports = function(crowi, req, locals) {
   locals.aclService = aclService;
   locals.customizeService = customizeService;
   locals.passportService = passportService;
+  locals.pageService = pageService;
   locals.pathUtils = pathUtils;
 
   locals.noCdn = function() {

+ 1 - 1
packages/app/src/server/views/widget/page_content.html

@@ -15,7 +15,7 @@
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
   data-page-is-not-creatable="false"
-  data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
+  data-page-is-able-to-delete-completely="{% if pageService.canDeleteCompletely(page.creator._id, user) %}true{% else %}false{% endif %}"
   data-slack-channels="{% if page %}{{ page.slackChannels }}{% endif %}"
   data-page-created-at="{{ page.createdAt|datetz('Y/m/d H:i:s') }}"
   data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"

+ 21 - 0
packages/app/src/stores/bookmarks.tsx

@@ -0,0 +1,21 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { Types } from 'mongoose';
+import { apiv3Get } from '~/client/util/apiv3-client';
+
+import { IBookmarksInfo } from '~/interfaces/bookmarks';
+
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSWRxBookmarksInfo = <Data, Error>(pageId: Types.ObjectId):SWRResponse<IBookmarksInfo, Error> => {
+  return useSWR(
+    ['/bookmarks/info', pageId],
+    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then((response) => {
+      return {
+        isBookmarked: response.data.isBookmarked,
+        sumOfBookmarks: response.data.sumOfBookmarks,
+        bookmarkedUsers: response.data.bookmarkedUsers,
+      };
+    }),
+  );
+};

+ 2 - 2
packages/app/src/stores/context.tsx

@@ -22,8 +22,8 @@ export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<
 };
 
 
-export const usePageId = (initialData?: Nullable<string>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('pageId', initialData ?? null);
+export const usePageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
+  return useStaticSWR<Nullable<string>, Error>('pageId', initialData ?? null);
 };
 
 export const useRevisionCreatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 6 - 3
packages/app/src/stores/page.tsx

@@ -47,12 +47,15 @@ export const useSWRxPageList = (
   );
 };
 
+type GetSubscriptionStatusResult = { subscribing: boolean };
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxSubscriptionStatus = <Data, Error>(pageId: Types.ObjectId): SWRResponse<{status: boolean | null}, Error> => {
+export const useSWRxSubscriptionStatus = <Data, Error>(pageId: string): SWRResponse<{status: boolean | null}, Error> => {
   const { data: isGuestUser } = useIsGuestUser();
+  const key = isGuestUser === false ? ['/page/subscribe', pageId] : null;
   return useSWR(
-    isGuestUser === false ? ['/page/subscribe', pageId] : null,
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then((response) => {
+    key,
+    (endpoint, pageId) => apiv3Get<GetSubscriptionStatusResult>(endpoint, { pageId }).then((response) => {
       return {
         status: response.data.subscribing,
       };

+ 1 - 1
packages/app/src/styles/_sidebar.scss

@@ -104,7 +104,7 @@
         width: 240px;
         height: 100%;
         &:not(.dragging) {
-          transition: width 300ms cubic-bezier(0.2, 0, 0, 1) 0s;
+          transition: width 200ms cubic-bezier(0.2, 0, 0, 1) 0s;
         }
         will-change: width;
         .grw-contextual-navigation-child {

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [
@@ -16,7 +16,6 @@
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
     "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
     "lint:js": "eslint **/*.{js,ts}",
-    "lint:styles": "stylelint src/styles/scss/**/*.scss",
     "lint": "npm-run-all -p lint:*",
     "test": "jest --verbose"
   },

+ 2 - 3
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
     "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
     "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
-    "lint:styles": "stylelint src/**/*.scss",
+    "lint:styles": "stylelint src/**/*.scss src/**/*.css",
     "lint": "run-p lint:*",
     "test": ""
   },
@@ -31,7 +31,6 @@
   },
   "devDependencies": {
     "npm-run-all": "^4.1.5",
-    "prettier-stylelint": "^0.4.2",
     "react": "^16.8.3",
     "react-dom": "^16.8.3"
   }

+ 2 - 2
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [
@@ -17,7 +17,7 @@
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
     "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
     "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
-    "lint:styles": "stylelint src/**/*.scss",
+    "lint:styles": "stylelint src/**/*.scss src/**/*.css",
     "lint": "run-p lint:*",
     "test": ""
   },

+ 1 - 3
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [
@@ -22,8 +22,6 @@
   },
   "devDependencies": {
     "browser-bunyan": "^1.6.3",
-    "stylelint": "^14.0.1",
-    "stylelint-config-recess-order": "^2.0.1",
     "tsc-alias": "^1.2.9"
   }
 }

+ 1 - 1
packages/slack/package.json

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

+ 49 - 36
packages/slackbot-proxy/docker/Dockerfile

@@ -1,46 +1,60 @@
-# syntax = docker/dockerfile:1.2
+# syntax = docker/dockerfile:1
 
 ##
-## deps-resolver-base
+## packages-json-picker
 ##
-FROM node:14-slim AS deps-resolver-base
+FROM node:14-slim AS packages-json-picker
 
-ENV appDir /opt
-
-WORKDIR ${appDir}
-COPY ./package.json ./
-COPY ./yarn.lock ./
-COPY ./lerna.json ./
-COPY ./packages/slack/package.json ./packages/slack/package.json
-COPY ./packages/slackbot-proxy/package.json ./packages/slackbot-proxy/package.json
-
-# setup
-RUN yarn config set network-timeout 300000
+ENV optDir /opt
 
+WORKDIR ${optDir}
+COPY ["package.json", "yarn.lock", "lerna.json", "./"]
+COPY packages packages
+# Find and remove non-package.json files
+RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
 
 
 ##
 ## deps-resolver-dev
 ##
-FROM deps-resolver-base AS deps-resolver-dev
-RUN npx lerna bootstrap
+FROM node:14-slim AS deps-resolver-dev
+
+ENV optDir /opt
+
+WORKDIR ${optDir}
+
+# copy files
+COPY --from=packages-json-picker ${optDir} .
+
+# setup
+RUN yarn config set network-timeout 300000
+RUN npx lerna bootstrap -- --frozen-lockfile
 
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/slackbot-proxy/node_modules \
-  packages/slack/node_modules
+RUN tar -cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
+
 
 
 ##
 ## deps-resolver-prod
 ##
-FROM deps-resolver-base AS deps-resolver-prod
+FROM node:14-slim AS deps-resolver-prod
+
+ENV optDir /opt
+
+WORKDIR ${optDir}
+COPY ["package.json", "yarn.lock", "lerna.json", "./"]
+COPY ./packages/slack/package.json ./packages/slack/package.json
+COPY ./packages/slackbot-proxy/package.json ./packages/slackbot-proxy/package.json
+
 RUN npx lerna bootstrap -- --production
 # make artifacts
-RUN tar cf dependencies.tar \
+RUN tar -cf dependencies.tar \
   node_modules \
-  packages/slack/node_modules \
-  packages/slackbot-proxy/node_modules
+  packages/*/node_modules
+
 
 
 ##
@@ -48,21 +62,19 @@ RUN tar cf dependencies.tar \
 ##
 FROM node:14-slim AS builder
 
-ENV appDir /opt
+ENV optDir /opt
 
-WORKDIR ${appDir}
+WORKDIR ${optDir}
 
 # copy dependent packages
 COPY --from=deps-resolver-dev \
-  ${appDir}/node_modules.tar ${appDir}/
+  ${optDir}/node_modules.tar ${optDir}/
 
 # extract node_modules.tar
-RUN tar xf node_modules.tar
+RUN tar -xf node_modules.tar
 RUN rm node_modules.tar
 
-COPY ./package.json ./
-COPY ./lerna.json ./
-COPY ./tsconfig.base.json ./
+COPY ["package.json", "lerna.json", "tsconfig.base.json", "./"]
 # copy all related packages
 COPY packages/slack packages/slack
 COPY packages/slackbot-proxy packages/slackbot-proxy
@@ -71,7 +83,7 @@ COPY packages/slackbot-proxy packages/slackbot-proxy
 RUN yarn lerna run build
 
 # make artifacts
-RUN tar cf packages.tar \
+RUN tar -cf packages.tar \
   packages/slack/package.json \
   packages/slack/dist \
   packages/slackbot-proxy/package.json \
@@ -88,13 +100,14 @@ LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 ENV NODE_ENV production
 
-ENV appDir /opt
+ENV optDir /opt
+ENV appDir ${optDir}
 
 # copy artifacts
 COPY --from=deps-resolver-prod --chown=node:node \
-  ${appDir}/dependencies.tar ${appDir}/
+  ${optDir}/dependencies.tar ${appDir}/
 COPY --from=builder --chown=node:node \
-  ${appDir}/packages.tar ${appDir}/
+  ${optDir}/packages.tar ${appDir}/
 
 RUN chown node:node ${appDir}
 
@@ -102,8 +115,8 @@ USER node
 
 # extract artifacts
 WORKDIR ${appDir}
-RUN tar xf dependencies.tar
-RUN tar xf packages.tar
+RUN tar -xf dependencies.tar
+RUN tar -xf packages.tar
 RUN rm dependencies.tar packages.tar
 
 WORKDIR ${appDir}/packages/slackbot-proxy

+ 5 - 6
packages/slackbot-proxy/docker/Dockerfile.dockerignore

@@ -1,6 +1,5 @@
-node_modules
-*/node_modules
-*/coverage
-*/dist
-*/Dockerfile
-*/*.dockerignore
+**/node_modules
+**/dist
+**/coverage
+**/Dockerfile
+**/*.dockerignore

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

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

+ 1 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "4.5.6-RC.0",
+  "version": "4.5.9-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [
@@ -12,7 +12,6 @@
   ],
   "scripts": {
     "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
-    "lint:styles": "stylelint src/styles/scss/**/*.scss",
     "lint": "npm-run-all -p lint:*",
     "test": "jest --verbose"
   },

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


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