Browse Source

Merge branch 'feat/77524-search-result-conent-page' into feat/77524-80789-templateModal-refactoring

* feat/77524-search-result-conent-page: (97 commits)
  refs #80335: Detach page control from page container - Fix import of api v3 client
  refs #80335: Detach page control from page container - Rename param name for rename modal.
  refs #80335: Detach page control from page container - Detach PageDuplicateModal from PageContainer
  support: Bump mpath with mongoose (#4638)
  fix lint error
  fix id (#4639)
  #79831 fix props
  update SearchResultList
  typescriptize
  refs #80335: Detach page control from page container - Detach BookmarkButton from PageContainer
  refs #80335: Detach page control from page container - Detach PageRenameModal from PageContainer
  refs #80335: Detach page control from page container - small changes
  refs #80335: Detach page control from page container - Detach PutBackPageModal from PageContainer and AppContainer(, which provided by Unstated)
  refs #80335: Detach page control from page container - Retry: Detach PageDeleteModal from PageContainer and AppContainer
  ci(deps): bump passport-oauth2 from 1.4.0 to 1.6.1 (#4599)
  ci(deps): bump passport from 0.4.0 to 0.5.0 (#4582)
  ci(deps): bump axios from 0.21.1 to 0.24.0 (#4604)
  ci(deps): bump tar from 4.4.13 to 4.4.19 (#4601)
  Update CHANGELOG.md
  Bump version
  ...
Mao 4 years ago
parent
commit
49542f1571
45 changed files with 706 additions and 731 deletions
  1. 26 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 2 2
      packages/app/docker/README.md
  5. 15 14
      packages/app/package.json
  6. 1 0
      packages/app/resource/locales/en_US/translation.json
  7. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  8. 1 0
      packages/app/resource/locales/zh_CN/translation.json
  9. 1 50
      packages/app/src/client/services/PageContainer.js
  10. 24 12
      packages/app/src/components/BookmarkButton.jsx
  11. 15 1
      packages/app/src/components/Navbar/SubNavButtons.jsx
  12. 9 1
      packages/app/src/components/Page/PageManagement.jsx
  13. 4 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  14. 17 11
      packages/app/src/components/PageDeleteModal.jsx
  15. 7 5
      packages/app/src/components/PageDuplicateModal.jsx
  16. 8 1
      packages/app/src/components/PageEditor/DrawioModal.jsx
  17. 9 9
      packages/app/src/components/PageList.jsx
  18. 25 22
      packages/app/src/components/PageRenameModal.jsx
  19. 2 2
      packages/app/src/components/PageTimeline.jsx
  20. 23 29
      packages/app/src/components/PaginationWrapper.tsx
  21. 13 12
      packages/app/src/components/PutbackPageModal.jsx
  22. 72 33
      packages/app/src/components/SearchPage.jsx
  23. 0 39
      packages/app/src/components/SearchPage/DeleteAllButton.jsx
  24. 62 0
      packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx
  25. 23 0
      packages/app/src/components/SearchPage/SearchControl.tsx
  26. 2 2
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  27. 0 32
      packages/app/src/components/SearchPage/SearchResultList.jsx
  28. 57 0
      packages/app/src/components/SearchPage/SearchResultList.tsx
  29. 78 45
      packages/app/src/components/SearchPage/SearchResultListItem.tsx
  30. 12 5
      packages/app/src/interfaces/page.ts
  31. 5 0
      packages/app/src/interfaces/search.ts
  32. 1 1
      packages/app/src/server/models/editor-settings.ts
  33. 8 7
      packages/app/src/server/routes/apiv3/page.js
  34. 4 1
      packages/app/src/styles/_layout.scss
  35. 2 2
      packages/app/src/styles/_search.scss
  36. 5 9
      packages/app/src/styles/theme/_apply-colors.scss
  37. 1 1
      packages/codemirror-textlint/package.json
  38. 1 1
      packages/core/package.json
  39. 1 1
      packages/plugin-attachment-refs/package.json
  40. 1 1
      packages/plugin-lsx/package.json
  41. 2 2
      packages/plugin-pukiwiki-like-linker/package.json
  42. 2 2
      packages/slack/package.json
  43. 3 3
      packages/slackbot-proxy/package.json
  44. 1 1
      packages/ui/package.json
  45. 158 368
      yarn.lock

+ 26 - 1
CHANGELOG.md

@@ -1,9 +1,34 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.4.9...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.4.10...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v4.4.10](https://github.com/weseek/growi/compare/v4.4.9...v4.4.10) - 2021-11-08
+
+### 🚀 Improvement
+
+- imprv: Sidebar content header style (#4526) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: /pages/info API is broken (#4602) @yuki-takei
+- fix: blackboard theme location in theme list (#4506) @ayaka0417
+
+### 🧰 Maintenance
+
+- support: Use SWR (#4487) @yuki-takei
+- support: Replaced PageList with SWR (#4498) @takayuki-t
+- support: Improve devcontainer (#4510) @yuki-takei
+- support: Update passport-ldpauth from ^2.0.0 to ^3.0.1 (#4578) @LuqmanHakim-Grune
+- ci(deps): bump validator from 13.6.0 to 13.7.0 (#4588) @dependabot
+- ci(deps-dev): bump stylelint from 13.2.0 to 14.0.1 (#4583) @dependabot
+- Bump browserslist from 4.0.1 to 4.16.6 (#3776) @dependabot
+- ci(deps-dev): bump colors from 1.2.5 to 1.4.0 (#4365) @dependabot
+- ci(deps-dev): bump on-headers from 1.0.1 to 1.0.2 (#4366) @dependabot
+- ci(deps-dev): bump jquery-ui from 1.12.1 to 1.13.0 (#4549) @dependabot
+- docs(page): Add docs to /page/info api (#4531) @Mxchaeltrxn
+
 ## [v4.4.9](https://github.com/weseek/growi/compare/v4.4.8...v4.4.9) - 2021-10-18
 
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.4.9`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.9/docker/Dockerfile)
-* [`4.4.9-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.9/docker/Dockerfile)
+* [`4.4.10`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/docker/Dockerfile)
+* [`4.4.10-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/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)
 

+ 15 - 14
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-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",
+    "lint:styles": "stylelint src/**/*.scss --custom-syntax postcss-scss",
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
@@ -51,17 +51,18 @@
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
+    "mongoose": "5.13.13 causes an error like 't.versions.node is undefined' about 'browser.umd.js' on browser",
     "string-width": "5.0.0 or above exports only ESM."
   },
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.10-RC.0",
-    "@growi/plugin-attachment-refs": "^4.4.10-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.10-RC.0",
-    "@growi/plugin-lsx": "^4.4.10-RC.0",
-    "@growi/slack": "^4.4.10-RC.0",
+    "@growi/codemirror-textlint": "^4.4.11-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.11-RC.0",
+    "@growi/plugin-lsx": "^4.4.11-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.11-RC.0",
+    "@growi/slack": "^4.4.11-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -72,7 +73,7 @@
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "aws-sdk": "^2.88.0",
-    "axios": "^0.21.1",
+    "axios": "^0.24.0",
     "body-parser": "^1.18.2",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
@@ -111,7 +112,7 @@
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.2",
     "mkdirp": "^1.0.3",
-    "mongoose": "5.12.13",
+    "mongoose": "=5.13.12",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
@@ -121,7 +122,7 @@
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
-    "passport": "^0.4.0",
+    "passport": "^0.5.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
@@ -157,7 +158,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.10-RC.0",
+    "@growi/ui": "^4.4.11-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -201,7 +202,6 @@
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.10.0",
     "node-dev": "^4.0.0",
-    "node-sass": "^4.14.1",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
@@ -223,13 +223,14 @@
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
-    "sass-loader": "^8.0.0",
+    "sass": "^1.43.4",
+    "sass-loader": "^10.1.1",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
-    "stylelint": "^13.2.0",
+    "stylelint": "^14.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger2openapi": "^5.3.1",
     "swr": "^1.0.1",

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

@@ -148,6 +148,7 @@
   "Sign out": "Logout",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
+  "Add to bookmark": "Add to bookmark",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
   "original_path":"Original path",

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

@@ -150,6 +150,7 @@
   "Sidebar mode": "サイドバーモード",
   "Sidebar mode on Editor": "サイドバーモード(編集時)",
   "No bookmarks yet": "No bookmarks yet",
+  "Add to bookmark": "ブックマークに追加",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
   "original_path":"元のパス",

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

@@ -156,6 +156,7 @@
 	"Sign out": "退出",
   "Disassociate": "解除关联",
   "No bookmarks yet": "暂无书签",
+  "Add to bookmark": "添加到书签",
 	"Recent Created": "最新创建",
   "Recent Changes": "最新修改",
   "original_path":"Original path",

+ 1 - 50
packages/app/src/client/services/PageContainer.js

@@ -277,7 +277,7 @@ export default class PageContainer extends Container {
         data: {
           likerIds, sumOfLikers, isLiked, seenUserIds, sumOfSeenUsers, isSeen,
         },
-      } = await this.appContainer.apiv3Get('/page/info', { _id: this.state.pageId });
+      } = await this.appContainer.apiv3Get('/page/info', { pageId: this.state.pageId });
 
       await this.setState({
         sumOfLikers,
@@ -328,12 +328,6 @@ export default class PageContainer extends Container {
     });
   }
 
-  async toggleBookmark() {
-    const bool = !this.state.isBookmarked;
-    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
-    return this.retrieveBookmarkInfo();
-  }
-
   async checkAndUpdateImageUrlCached(users) {
     const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
     if (noImageCacheUsers.length === 0) {
@@ -529,49 +523,6 @@ export default class PageContainer extends Container {
     return res;
   }
 
-  deletePage(isRecursively, isCompletely) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const completely = isCompletely ? true : null;
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.remove', {
-      recursively,
-      completely,
-      page_id: this.state.pageId,
-      revision_id: this.state.revisionId,
-    });
-
-  }
-
-  revertRemove(isRecursively) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.revertRemove', {
-      recursively,
-      page_id: this.state.pageId,
-    });
-  }
-
-  rename(newPagePath, isRecursively, isRenameRedirect, isRemainMetadata) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const { pageId, revisionId, path } = this.state;
-
-    return this.appContainer.apiv3Put('/pages/rename', {
-      revisionId,
-      pageId,
-      isRecursively,
-      isRenameRedirect,
-      isRemainMetadata,
-      newPagePath,
-      path,
-    });
-  }
-
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,

+ 24 - 12
packages/app/src/components/BookmarkButton.jsx

@@ -6,7 +6,8 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import { toastError } from '~/client/util/apiNotification';
-import PageContainer from '~/client/services/PageContainer';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
 import AppContainer from '~/client/services/AppContainer';
 
 class BookmarkButton extends React.Component {
@@ -18,7 +19,9 @@ class BookmarkButton extends React.Component {
   }
 
   async handleClick() {
-    const { appContainer, pageContainer } = this.props;
+    const {
+      appContainer, pageId, isBookmarked, onChangeInvoked,
+    } = this.props;
     const { isGuestUser } = appContainer;
 
     if (isGuestUser) {
@@ -26,16 +29,21 @@ class BookmarkButton extends React.Component {
     }
 
     try {
-      pageContainer.toggleBookmark();
+      const bool = !isBookmarked;
+      await apiv3Put('/bookmarks', { pageId, bool });
+      if (onChangeInvoked != null) {
+        onChangeInvoked();
+      }
     }
     catch (err) {
       toastError(err);
     }
   }
 
-
   render() {
-    const { appContainer, pageContainer, t } = this.props;
+    const {
+      appContainer, t, isBookmarked, sumOfBookmarks,
+    } = this.props;
     const { isGuestUser } = appContainer;
 
     return (
@@ -45,12 +53,14 @@ class BookmarkButton extends React.Component {
           id="bookmark-button"
           onClick={this.handleClick}
           className={`btn btn-bookmark border-0
-          ${`btn-${this.props.size}`} ${pageContainer.state.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          ${`btn-${this.props.size}`} ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
           <i className="icon-star mr-3"></i>
-          <span className="total-bookmarks">
-            {pageContainer.state.sumOfBookmarks}
-          </span>
+          {sumOfBookmarks && (
+            <span className="total-bookmarks">
+              {sumOfBookmarks}
+            </span>
+          )}
         </button>
 
         {isGuestUser && (
@@ -67,13 +77,15 @@ class BookmarkButton extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer, PageContainer]);
+const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer]);
 
 BookmarkButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
-  pageId: PropTypes.string,
+  pageId: PropTypes.string.isRequired,
+  isBookmarked: PropTypes.bool.isRequired,
+  sumOfBookmarks: PropTypes.number,
+  onChangeInvoked: PropTypes.func,
   t: PropTypes.func.isRequired,
   size: PropTypes.string,
 };

+ 15 - 1
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -4,11 +4,14 @@ import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
+import loggerFactory from '~/utils/logger';
 
 import BookmarkButton from '../BookmarkButton';
 import LikeButtons from '../LikeButtons';
 import PageManagement from '../Page/PageManagement';
 
+const logger = loggerFactory('growi:SubnavButtons');
+
 const SubnavButtons = (props) => {
   const {
     appContainer, navigationContainer, pageContainer, isCompactMode,
@@ -18,6 +21,12 @@ const SubnavButtons = (props) => {
 
   /* eslint-disable react/prop-types */
   const PageReactionButtons = ({ pageContainer }) => {
+    const { pageId, isBookmarked, sumOfBookmarks } = pageContainer.state;
+
+    const onChangeInvoked = () => {
+      if (pageContainer.retrieveBookmarkInfo == null) { logger.error('retrieveBookmarkInfo is null') }
+      else { pageContainer.retrieveBookmarkInfo() }
+    };
 
     return (
       <>
@@ -27,7 +36,12 @@ const SubnavButtons = (props) => {
           </span>
         )}
         <span>
-          <BookmarkButton />
+          <BookmarkButton
+            pageId={pageId}
+            isBookmarked={isBookmarked}
+            sumOfBookmarks={sumOfBookmarks}
+            onChangeInvoked={onChangeInvoked}
+          />
         </span>
       </>
     );

+ 9 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -22,7 +22,9 @@ const PageManagement = (props) => {
   const {
     t, appContainer, pageContainer, isCompactMode,
   } = props;
-  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
+  const {
+    pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
+  } = pageContainer.state;
 
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
@@ -165,11 +167,15 @@ const PageManagement = (props) => {
         <PageRenameModal
           isOpen={isPageRenameModalShown}
           onClose={closePageRenameModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
         />
         <PageDuplicateModal
           isOpen={isPageDuplicateModalShown}
           onClose={closePageDuplicateModalHandler}
+          pageId={pageId}
+          path={path}
         />
         <CreateTemplateModal
           path={path}
@@ -179,6 +185,8 @@ const PageManagement = (props) => {
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           onClose={closePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
         />

+ 4 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -15,7 +15,7 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const {
-    path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    pageId, revisionId, path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
   } = pageContainer.state;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
@@ -92,11 +92,14 @@ const TrashPageAlert = (props) => {
         <PutbackPageModal
           isOpen={isPutbackPageModalShown}
           onClose={closePutbackPageModalHandler}
+          pageId={pageId}
           path={path}
         />
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           onClose={opclosePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           isDeleteCompletelyModal
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}

+ 17 - 11
packages/app/src/components/PageDeleteModal.jsx

@@ -7,8 +7,7 @@ import {
 
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
@@ -27,7 +26,7 @@ const deleteIconAndKey = {
 
 const PageDeleteModal = (props) => {
   const {
-    t, pageContainer, isOpen, onClose, isDeleteCompletelyModal, path, isAbleToDeleteCompletely,
+    t, isOpen, onClose, isDeleteCompletelyModal, pageId, revisionId, path, isAbleToDeleteCompletely,
   } = props;
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
@@ -50,7 +49,18 @@ const PageDeleteModal = (props) => {
     setErrs(null);
 
     try {
-      const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isDeleteRecursively ? true : null;
+      const completely = isDeleteCompletely ? true : null;
+
+      const response = await apiPost('/pages.remove', {
+        page_id: pageId,
+        revision_id: revisionId,
+        recursively,
+        completely,
+      });
+
       const trashPagePath = response.page.path;
       window.location.href = encodeURI(trashPagePath);
     }
@@ -133,18 +143,14 @@ const PageDeleteModal = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PageDeleteModalWrapper = withUnstatedContainers(PageDeleteModal, [PageContainer]);
-
 PageDeleteModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   isDeleteCompletelyModal: PropTypes.bool,
   isAbleToDeleteCompletely: PropTypes.bool,
@@ -154,4 +160,4 @@ PageDeleteModal.defaultProps = {
   isDeleteCompletelyModal: false,
 };
 
-export default withTranslation()(PageDeleteModalWrapper);
+export default withTranslation()(PageDeleteModal);

+ 7 - 5
packages/app/src/components/PageDuplicateModal.jsx

@@ -11,7 +11,6 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
@@ -20,11 +19,12 @@ import DuplicatePathsTable from './DuplicatedPathsTable';
 const LIMIT_FOR_LIST = 10;
 
 const PageDuplicateModal = (props) => {
-  const { t, appContainer, pageContainer } = props;
+  const {
+    t, appContainer, pageId, path,
+  } = props;
 
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
-  const { pageId, path } = pageContainer.state;
   const { crowi } = appContainer.config;
 
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -213,16 +213,18 @@ const PageDuplicateModal = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer, PageContainer]);
+const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer]);
 
 
 PageDuplicateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
+
+  pageId: PropTypes.string.isRequired,
+  path: PropTypes.string.isRequired,
 };
 
 export default withTranslation()(PageDuplicateModallWrapper);

+ 8 - 1
packages/app/src/components/PageEditor/DrawioModal.jsx

@@ -135,7 +135,14 @@ class DrawioModal extends React.PureComponent {
 
   render() {
     return (
-      <Modal isOpen={this.state.show} toggle={this.cancel} className="drawio-modal" size="xl" keyboard={false}>
+      <Modal
+        isOpen={this.state.show}
+        toggle={this.cancel}
+        backdrop="static"
+        className="drawio-modal"
+        size="xl"
+        keyboard={false}
+      >
         <ModalBody className="p-0">
           {/* Loading spinner */}
           <div className="w-100 h-100 position-absolute d-flex">

+ 9 - 9
packages/app/src/components/PageList.jsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useCallback, useState } from 'react';
+import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
@@ -8,7 +8,6 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
-import { toastError } from '~/client/util/apiNotification';
 import { useSWRxPageList } from '~/stores/page';
 
 import PaginationWrapper from './PaginationWrapper';
@@ -20,18 +19,19 @@ const PageList = (props) => {
 
   const [activePage, setActivePage] = useState(1);
 
-  const { data: pagesListData, error } = useSWRxPageList(path, activePage);
+  const { data: pagesListData, error: errors } = useSWRxPageList(path, activePage);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
   }
 
-
-  // TODO: To be implemented in #79549
-  if (error != null) {
-    // toastError(error, 'Error occurred in PageList');
-    // eslint-disable-next-line no-console
-    console.log(error, 'Error occurred in PageList');
+  if (errors != null) {
+    return (
+      <div className="my-5">
+        {/* eslint-disable-next-line react/no-array-index-key */}
+        {errors.map((error, index) => <div key={index} className="text-danger">{error.message}</div>)}
+      </div>
+    );
   }
 
   if (pagesListData == null) {

+ 25 - 22
packages/app/src/components/PageRenameModal.jsx

@@ -14,7 +14,9 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
+
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
 import DuplicatedPathsTable from './DuplicatedPathsTable';
@@ -22,11 +24,9 @@ import DuplicatedPathsTable from './DuplicatedPathsTable';
 
 const PageRenameModal = (props) => {
   const {
-    t, appContainer, pageContainer,
+    t, appContainer, path, pageId, revisionId,
   } = props;
 
-  const { path } = pageContainer.state;
-
   const { crowi } = appContainer.config;
 
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -37,7 +37,7 @@ const PageRenameModal = (props) => {
   const [existingPaths, setExistingPaths] = useState([]);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
-  const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
+  const [isRemainMetadata, SetIsRemainMetadata] = useState(false);
   const [subordinatedError] = useState(null);
   const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
 
@@ -53,13 +53,13 @@ const PageRenameModal = (props) => {
     SetIsRenameRedirect(!isRenameRedirect);
   }
 
-  function changeIsRenameMetadataHandler() {
-    SetIsRenameMetadata(!isRenameMetadata);
+  function changeIsRemainMetadataHandler() {
+    SetIsRemainMetadata(!isRemainMetadata);
   }
 
   const updateSubordinatedList = useCallback(async() => {
     try {
-      const res = await appContainer.apiv3Get('/pages/subordinated-list', { path });
+      const res = await apiv3Get('/pages/subordinated-list', { path });
       const { subordinatedPaths } = res.data;
       setSubordinatedPages(subordinatedPaths);
     }
@@ -67,7 +67,7 @@ const PageRenameModal = (props) => {
       setErrs(err);
       toastError(t('modal_rename.label.Fail to get subordinated pages'));
     }
-  }, [appContainer, path, t]);
+  }, [path, t]);
 
   useEffect(() => {
     if (props.isOpen) {
@@ -78,7 +78,7 @@ const PageRenameModal = (props) => {
 
   const checkExistPaths = async(newParentPath) => {
     try {
-      const res = await appContainer.apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
+      const res = await apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
       const { existPaths } = res.data;
       setExistingPaths(existPaths);
     }
@@ -112,12 +112,15 @@ const PageRenameModal = (props) => {
     setErrs(null);
 
     try {
-      const response = await pageContainer.rename(
-        pageNameInput,
-        isRenameRecursively,
+      const response = await apiv3Put('/pages/rename', {
+        revisionId,
+        pageId,
+        isRecursively: isRenameRecursively,
         isRenameRedirect,
-        isRenameMetadata,
-      );
+        isRemainMetadata,
+        newPagePath: pageNameInput,
+        path,
+      });
 
       const { page } = response.data;
       const url = new URL(page.path, 'https://dummy');
@@ -215,12 +218,12 @@ const PageRenameModal = (props) => {
           <input
             className="custom-control-input"
             name="remain_metadata"
-            id="cbRenameMetadata"
+            id="cbRemainMetadata"
             type="checkbox"
-            checked={isRenameMetadata}
-            onChange={changeIsRenameMetadataHandler}
+            checked={isRemainMetadata}
+            onChange={changeIsRemainMetadataHandler}
           />
-          <label className="custom-control-label" htmlFor="cbRenameMetadata">
+          <label className="custom-control-label" htmlFor="cbRemainMetadata">
             { t('modal_rename.label.Do not update metadata') }
             <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
           </label>
@@ -244,17 +247,17 @@ const PageRenameModal = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer, PageContainer]);
-
+const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer]);
 
 PageRenameModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 

+ 2 - 2
packages/app/src/components/PageTimeline.jsx

@@ -76,14 +76,14 @@ class PageTimeline extends React.Component {
       <div>
         { pages.map((page) => {
           return (
-            <div className="timeline-body" key={`key-${page.id}`}>
+            <div className="timeline-body" key={`key-${page._id}`}>
               <div className="card card-timeline">
                 <div className="card-header"><a href={page.path}>{page.path}</a></div>
                 <div className="card-body">
                   <RevisionLoader
                     lazy
                     growiRenderer={this.growiRenderer}
-                    pageId={page.id}
+                    pageId={page._id}
                     revisionId={page.revision}
                   />
                 </div>

+ 23 - 29
packages/app/src/components/PaginationWrapper.jsx → packages/app/src/components/PaginationWrapper.tsx

@@ -1,18 +1,21 @@
-import React, { useCallback, useMemo } from 'react';
-import PropTypes from 'prop-types';
+import React, {
+  FC, memo, useCallback, useMemo,
+} from 'react';
 
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 
-/**
- *
- * @author Mikitaka Itizawa <itizawa@weseek.co.jp>
- *
- * @export
- * @class PaginationWrapper
- * @extends {React.Component}
- */
 
-const PaginationWrapper = React.memo((props) => {
+type Props = {
+  activePage: number,
+  changePage?: (number) => void,
+  totalItemsCount: number,
+  pagingLimit?: number,
+  align?: string,
+  size?: string,
+};
+
+
+const PaginationWrapper: FC<Props> = memo((props: Props) => {
   const {
     activePage, changePage, totalItemsCount, pagingLimit, align,
   } = props;
@@ -59,14 +62,14 @@ const PaginationWrapper = React.memo((props) => {
    * this function set << & <
    */
   const generateFirstPrev = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     if (activePage !== 1) {
       paginationItems.push(
         <PaginationItem key="painationItemFirst">
-          <PaginationLink first onClick={() => { return changePage(1) }} />
+          <PaginationLink first onClick={() => { return changePage != null && changePage(1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemPrevious">
-          <PaginationLink previous onClick={() => { return changePage(activePage - 1) }} />
+          <PaginationLink previous onClick={() => { return changePage != null && changePage(activePage - 1) }} />
         </PaginationItem>,
       );
     }
@@ -89,11 +92,11 @@ const PaginationWrapper = React.memo((props) => {
    * this function set  numbers
    */
   const generatePaginations = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
       paginationItems.push(
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
-          <PaginationLink onClick={() => { return changePage(number) }}>
+          <PaginationLink onClick={() => { return changePage != null && changePage(number) }}>
             {number}
           </PaginationLink>
         </PaginationItem>,
@@ -108,14 +111,14 @@ const PaginationWrapper = React.memo((props) => {
    * this function set > & >>
    */
   const generateNextLast = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     if (totalPage !== activePage) {
       paginationItems.push(
         <PaginationItem key="painationItemNext">
-          <PaginationLink next onClick={() => { return changePage(activePage + 1) }} />
+          <PaginationLink next onClick={() => { return changePage != null && changePage(activePage + 1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemLast">
-          <PaginationLink last onClick={() => { return changePage(totalPage) }} />
+          <PaginationLink last onClick={() => { return changePage != null && changePage(totalPage) }} />
         </PaginationItem>,
       );
     }
@@ -133,7 +136,7 @@ const PaginationWrapper = React.memo((props) => {
   }, [activePage, changePage, totalPage]);
 
   const getListClassName = useMemo(() => {
-    const listClassNames = [];
+    const listClassNames: string[] = [];
 
     if (align === 'center') {
       listClassNames.push('justify-content-center');
@@ -157,15 +160,6 @@ const PaginationWrapper = React.memo((props) => {
 
 });
 
-PaginationWrapper.propTypes = {
-  activePage: PropTypes.number.isRequired,
-  changePage: PropTypes.func.isRequired,
-  totalItemsCount: PropTypes.number.isRequired,
-  pagingLimit: PropTypes.number,
-  align: PropTypes.string,
-  size: PropTypes.string,
-};
-
 PaginationWrapper.defaultProps = {
   align: 'left',
   size: 'md',

+ 13 - 12
packages/app/src/components/PutbackPageModal.jsx

@@ -7,15 +7,13 @@ import {
 
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const PutBackPageModal = (props) => {
   const {
-    t, isOpen, onClose, pageContainer, path,
+    t, isOpen, onClose, pageId, path,
   } = props;
 
   const [errs, setErrs] = useState(null);
@@ -30,7 +28,15 @@ const PutBackPageModal = (props) => {
     setErrs(null);
 
     try {
-      const response = await pageContainer.revertRemove(isPutbackRecursively);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isPutbackRecursively ? true : null;
+
+      const response = await apiPost('/pages.revertRemove', {
+        page_id: pageId,
+        recursively,
+      });
+
       const putbackPagePath = response.page.path;
       window.location.href = encodeURI(putbackPagePath);
     }
@@ -80,20 +86,15 @@ const PutBackPageModal = (props) => {
 
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PutBackPageModalWrapper = withUnstatedContainers(PutBackPageModal, [PageContainer]);
-
 PutBackPageModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
 
+  pageId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 
 
-export default withTranslation()(PutBackPageModalWrapper);
+export default withTranslation()(PutBackPageModal);

+ 72 - 33
packages/app/src/components/SearchPage.jsx

@@ -30,18 +30,23 @@ class SearchPage extends React.Component {
       searchedKeyword: '',
       searchedPages: [],
       searchResultMeta: {},
-      selectedPage: null,
+      focusedPage: {},
       selectedPages: new Set(),
+      searchResultCount: 0,
+      activePage: 1,
+      pagingLimit: 3, // change to an appropriate limit number
       excludeUsersHome: true,
       excludeTrash: true,
     };
 
     this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
+    this.searchHandler = this.searchHandler.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
+    this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
   }
 
   componentDidMount() {
@@ -96,13 +101,34 @@ class SearchPage extends React.Component {
     return query;
   }
 
-  search(data) {
+  /**
+   * this method is called when user changes paging number
+   */
+  async onPagingNumberChanged(activePage) {
+    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
+    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+    this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
+  }
+
+  /**
+   * this method is called when user searches by pressing Enter or using searchbox
+   */
+  async searchHandler(data) {
+    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
+    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+    this.setState({ activePage: 1 }, () => this.search(data));
+  }
+
+  async search(data) {
     const keyword = data.keyword;
     if (keyword === '') {
       this.setState({
         searchingKeyword: '',
+        searchedKeyword: '',
         searchedPages: [],
         searchResultMeta: {},
+        searchResultCount: 0,
+        activePage: 1,
       });
 
       return true;
@@ -111,29 +137,40 @@ class SearchPage extends React.Component {
     this.setState({
       searchingKeyword: keyword,
     });
-    this.props.appContainer.apiGet('/search', { q: this.createSearchQuery(keyword) })
-      .then((res) => {
-        this.changeURL(keyword);
-        if (res.data.length > 0) {
-          this.setState({
-            searchedKeyword: keyword,
-            searchedPages: res.data,
-            searchResultMeta: res.meta,
-            selectedPage: res.data[0],
-          });
-        }
-        else {
-          this.setState({
-            searchedKeyword: keyword,
-            searchedPages: [],
-            searchResultMeta: {},
-            selectedPage: null,
-          });
-        }
-      })
-      .catch((err) => {
-        toastError(err);
+    const pagingLimit = this.state.pagingLimit;
+    const offset = (this.state.activePage * pagingLimit) - pagingLimit;
+    try {
+      const res = await this.props.appContainer.apiGet('/search', {
+        q: this.createSearchQuery(keyword),
+        limit: pagingLimit,
+        offset,
       });
+      this.changeURL(keyword);
+      if (res.data.length > 0) {
+        this.setState({
+          searchedKeyword: keyword,
+          searchedPages: res.data,
+          searchResultMeta: res.meta,
+          searchResultCount: res.meta.total,
+          focusedPage: res.data[0],
+          // reset active page if keyword changes, otherwise set the current state
+          activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
+        });
+      }
+      else {
+        this.setState({
+          searchedKeyword: keyword,
+          searchedPages: [],
+          searchResultMeta: {},
+          searchResultCount: 0,
+          focusedPage: {},
+          activePage: 1,
+        });
+      }
+    }
+    catch (err) {
+      toastError(err);
+    }
   }
 
   selectPage= (pageId) => {
@@ -141,7 +178,7 @@ class SearchPage extends React.Component {
       return page._id === pageId;
     });
     this.setState({
-      selectedPage: this.state.searchedPages[index],
+      focusedPage: this.state.searchedPages[index],
     });
   }
 
@@ -159,7 +196,7 @@ class SearchPage extends React.Component {
       <SearchResultContent
         appContainer={this.props.appContainer}
         searchingKeyword={this.state.searchingKeyword}
-        selectedPage={this.state.selectedPage}
+        focusedPage={this.state.focusedPage}
       >
       </SearchResultContent>
     );
@@ -168,14 +205,16 @@ class SearchPage extends React.Component {
   renderSearchResultList = () => {
     return (
       <SearchResultList
-        pages={this.state.searchedPages}
-        deletionMode={false}
-        selectedPage={this.state.selectedPage}
-        selectedPages={this.state.selectedPages}
+        pages={this.state.searchedPages || []}
+        focusedPage={this.state.focusedPage}
+        selectedPages={this.state.selectedPages || []}
+        searchResultCount={this.state.searchResultCount}
+        activePage={this.state.activePage}
+        pagingLimit={this.state.pagingLimit}
         onClickInvoked={this.selectPage}
         onChangedInvoked={this.toggleCheckBox}
-      >
-      </SearchResultList>
+        onPagingNumberChanged={this.onPagingNumberChanged}
+      />
     );
   }
 
@@ -184,7 +223,7 @@ class SearchPage extends React.Component {
       <SearchControl
         searchingKeyword={this.state.searchingKeyword}
         appContainer={this.props.appContainer}
-        onSearchInvoked={this.search}
+        onSearchInvoked={this.searchHandler}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeTrash={this.onExcludeTrash}
       >

+ 0 - 39
packages/app/src/components/SearchPage/DeleteAllButton.jsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-
-const DeleteAllButton = (props) => {
-  const { selectedPage, checked } = props;
-  const { t } = useTranslation();
-  function deleteAllSelectedPage(pagesToDelete) {
-    // TODO: implement this function
-    // https://estoc.weseek.co.jp/redmine/issues/77543
-    // do something with pagesDelete to delete them.
-  }
-  return (
-    <div>
-      <label>
-        <input
-          type="checkbox"
-          name="check-delte-all"
-          onChange={() => {
-            if (checked) {
-              deleteAllSelectedPage(selectedPage);
-            }
-          }}
-        />
-        <span className="text-danger font-weight-light">
-          <i className="icon-trash ml-3"></i>
-          {t('search_result.delete_all_selected_page')}
-        </span>
-      </label>
-    </div>
-  );
-
-};
-
-DeleteAllButton.propTypes = {
-  selectedPage: PropTypes.array.isRequired,
-  checked: PropTypes.bool.isRequired,
-};
-export default DeleteAllButton;

+ 62 - 0
packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx

@@ -0,0 +1,62 @@
+import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import loggerFactory from '~/utils/logger';
+import { CheckboxType } from '../../interfaces/search';
+
+const logger = loggerFactory('growi:searchResultList');
+
+type Props = {
+  checkboxState: CheckboxType,
+  onClickInvoked?: () => void,
+  onCheckInvoked?: (string:CheckboxType) => void,
+}
+
+const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
+  const { t } = useTranslation();
+  const {
+    checkboxState, onClickInvoked, onCheckInvoked,
+  } = props;
+
+  const changeCheckboxStateHandler = () => {
+    console.log(`changeCheckboxStateHandler is called. current changebox state is ${checkboxState}`);
+    // Todo: determine next checkboxState from one of the following and tell the parent component
+    // to change the checkboxState by passing onCheckInvoked function the next checkboxState
+    // - NONE_CHECKED
+    // - INDETERMINATE
+    // - ALL_CHECKED
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+    // use CheckboxType by importing from packages/app/src/interfaces/
+    if (onCheckInvoked == null) { logger.error('onCheckInvoked is null') }
+    else { onCheckInvoked(CheckboxType.ALL_CHECKED) } // change this to an appropriate value
+  };
+
+
+  return (
+    <>
+      <input
+        id="check-all-pages"
+        type="checkbox"
+        name="check-all-pages"
+        className="custom-control custom-checkbox ml-1 align-self-center"
+        onChange={changeCheckboxStateHandler}
+        checked={checkboxState === CheckboxType.INDETERMINATE || checkboxState === CheckboxType.ALL_CHECKED}
+      />
+      <button
+        type="button"
+        className="btn text-danger font-weight-light p-0 ml-3"
+        onClick={() => {
+          if (onClickInvoked == null) { logger.error('onClickInvoked is null') }
+          else { onClickInvoked() }
+        }}
+      >
+        <i className="icon-trash"></i>
+        {t('search_result.delete_all_selected_page')}
+      </button>
+    </>
+  );
+
+};
+
+DeleteSelectedPageGroup.propTypes = {
+};
+export default DeleteSelectedPageGroup;

+ 23 - 0
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -2,6 +2,8 @@ import React, { FC } from 'react';
 import { useTranslation } from 'react-i18next';
 import SearchPageForm from './SearchPageForm';
 import AppContainer from '../../client/services/AppContainer';
+import DeleteSelectedPageGroup from './DeleteSelectedPageGroup';
+import { CheckboxType } from '../../interfaces/search';
 
 type Props = {
   searchingKeyword: string,
@@ -29,6 +31,21 @@ const SearchControl: FC <Props> = (props: Props) => {
     }
   };
 
+  const onDeleteSelectedPageHandler = () => {
+    console.log('onDeleteSelectedPageHandler is called');
+    // TODO: implement this function to delete selected pages.
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+  };
+
+  const onCheckAllPagesInvoked = (nextCheckboxState:CheckboxType) => {
+    console.log(`onCheckAllPagesInvoked is called with arg ${nextCheckboxState}`);
+    // Todo: set the checkboxState, isChecked, and indeterminate value of checkbox element according to the passed argument
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+
+    // setting checkbox to indeterminate is required to use of useRef to access checkbox element.
+    // ref: https://getbootstrap.com/docs/4.5/components/forms/#checkboxes
+  };
+
   return (
     <div className="">
       <div className="search-page-input sps sps--abv">
@@ -40,6 +57,12 @@ const SearchControl: FC <Props> = (props: Props) => {
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       <div className="d-flex my-4">
+        {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
+        <DeleteSelectedPageGroup
+          checkboxState={'' || CheckboxType.NONE_CHECKED} // Todo: change the left value to appropriate value
+          onClickInvoked={onDeleteSelectedPageHandler}
+          onCheckInvoked={onCheckAllPagesInvoked}
+        />
         <div className="d-flex align-items-center border rounded border-gray px-2 py-1 mr-2 ml-auto">
           <label className="my-0 mr-2" htmlFor="flexCheckDefault">
             {t('Include Subordinated Target Page', { target: '/user' })}

+ 2 - 2
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -7,10 +7,10 @@ import SearchResultContentSubNavigation from './SearchResultContentSubNavigation
 type Props ={
   appContainer: AppContainer,
   searchingKeyword:string,
-  selectedPage : null | any,
+  focusedPage : any,
 }
 const SearchResultContent: FC<Props> = (props: Props) => {
-  const page = props.selectedPage;
+  const page = props.focusedPage;
   if (page == null) return null;
   // Temporaly workaround for lint error
   // later needs to be fixed: RevisoinRender to typescriptcomponet

+ 0 - 32
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,32 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import SearchResultListItem from './SearchResultListItem';
-
-class SearchResultList extends React.Component {
-
-  render() {
-    return this.props.pages.map((page) => {
-      // TODO : send cetain  length of body (revisionBody) from elastisearch by adding some settings to the query and
-      //         when keyword is not in page content, display revisionBody.
-      // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
-      return (
-        <SearchResultListItem
-          page={page}
-          onClickInvoked={this.props.onClickInvoked}
-          noLink
-        />
-      );
-    });
-  }
-
-}
-
-SearchResultList.propTypes = {
-  pages: PropTypes.array.isRequired,
-  deletionMode: PropTypes.bool.isRequired,
-  selectedPages: PropTypes.array.isRequired,
-  onClickInvoked: PropTypes.func,
-  onChangeInvoked: PropTypes.func,
-};
-
-export default SearchResultList;

+ 57 - 0
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -0,0 +1,57 @@
+import React, { FC } from 'react';
+import SearchResultListItem from './SearchResultListItem';
+import { IPageHasId } from '../../interfaces/page';
+import PaginationWrapper from '../PaginationWrapper';
+
+// TOOD: retrieve bookmark count and add it to the following type
+export type ISearchedPage = IPageHasId & {
+  snippet: string,
+  elasticSearchResult: {
+    snippet: string,
+    matchedPath: string,
+  },
+};
+
+type Props = {
+  pages: ISearchedPage[],
+  selectedPages: ISearchedPage[],
+  onClickInvoked?: (pageId: string) => void,
+  searchResultCount?: number,
+  activePage?: number,
+  pagingLimit?: number,
+  onPagingNumberChanged?: (activePage: number) => void,
+  focusedPage?: ISearchedPage,
+}
+
+const SearchResultList: FC<Props> = (props:Props) => {
+  const { focusedPage } = props;
+  const focusedPageId = focusedPage != null && focusedPage._id != null ? focusedPage._id : '';
+  return (
+    <>
+      {props.pages.map((page) => {
+        return (
+          <SearchResultListItem
+            key={page._id}
+            page={page}
+            onClickInvoked={props.onClickInvoked}
+            isSelected={page._id === focusedPageId || false}
+          />
+        );
+      })}
+      {props.searchResultCount != null && props.searchResultCount > 0 && (
+        <div className="my-4 mx-auto">
+          <PaginationWrapper
+            activePage={props.activePage || 1}
+            changePage={props.onPagingNumberChanged}
+            totalItemsCount={props.searchResultCount || 0}
+            pagingLimit={props.pagingLimit}
+          />
+        </div>
+      )}
+
+    </>
+  );
+
+};
+
+export default SearchResultList;

+ 78 - 45
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -1,28 +1,76 @@
 import React, { FC } from 'react';
-
+import { useTranslation } from 'react-i18next';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
+import { ISearchedPage } from './SearchResultList';
 
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:searchResultList');
 
-type Props ={
-  page: {
-    _id: string,
-    path: string,
-    noLink: boolean,
-    lastUpdateUser: any
-    elasticSearchResult: {
-      snippet: string,
-    }
-  },
-  onClickInvoked: (data: string) => void,
+type PageItemControlProps = {
+  page: ISearchedPage,
 }
 
-const SearchResultListItem: FC<Props> = (props:Props) => {
+const PageItemControl: FC<PageItemControlProps> = (props: {page: ISearchedPage}) => {
+
+  const { page } = props;
+  const { t } = useTranslation('');
+
+  return (
+    <>
+      <button
+        type="button"
+        className="btn-link nav-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded grw-btn-page-management py-0"
+        data-toggle="dropdown"
+      >
+        <i className="fa fa-ellipsis-v text-muted"></i>
+      </button>
+      <div className="dropdown-menu dropdown-menu-right">
 
-  const { page, onClickInvoked } = props;
+        {/* TODO: if there is the following button in XD add it here
+        <button
+          type="button"
+          className="btn btn-link p-0"
+          value={page.path}
+          onClick={(e) => {
+            window.location.href = e.currentTarget.value;
+          }}
+        >
+          <i className="icon-login" />
+        </button>
+        */}
+
+        {/*
+          TODO: add function to the following buttons like using modal or others
+          ref: https://estoc.weseek.co.jp/redmine/issues/79026
+        */}
+        <button className="dropdown-item text-danger" type="button" onClick={() => console.log('delete modal show')}>
+          <i className="icon-fw icon-fire"></i>{t('Delete')}
+        </button>
+        <button className="dropdown-item" type="button" onClick={() => console.log('duplicate modal show')}>
+          <i className="icon-fw icon-star"></i>{t('Add to bookmark')}
+        </button>
+        <button className="dropdown-item" type="button" onClick={() => console.log('duplicate modal show')}>
+          <i className="icon-fw icon-docs"></i>{t('Duplicate')}
+        </button>
+        <button className="dropdown-item" type="button" onClick={() => console.log('rename function will be added')}>
+          <i className="icon-fw  icon-action-redo"></i>{t('Move/Rename')}
+        </button>
+      </div>
+    </>
+  );
+
+};
+
+type Props = {
+  page: ISearchedPage,
+  isSelected: boolean,
+  onClickInvoked?: (pageId: string) => void,
+}
+
+const SearchResultListItem: FC<Props> = (props:Props) => {
+  const { page, isSelected } = props;
 
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
   const pageId = `#${page._id}`;
@@ -30,28 +78,22 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const pagePathElem = <PagePathLabel page={page} isFormerOnly />;
 
-  // TODO : send cetain  length of body (revisionBody) from elastisearch by adding some settings to the query and
-  //         when keyword is not in page content, display revisionBody.
-  // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
+  const onClickInvoked = (pageId) => {
+    if (props.onClickInvoked != null) {
+      props.onClickInvoked(pageId);
+    }
+  };
 
   return (
-    <li key={page._id} className="page-list-li w-100 border-bottom pr-4">
+    <li key={page._id} className={`page-list-li w-100 border-bottom pr-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
       <a
         className="d-block pt-3"
         href={pageId}
-        onClick={() => {
-          try {
-            if (onClickInvoked == null) { throw new Error('onClickInvoked is null') }
-            onClickInvoked(page._id);
-          }
-          catch (error) {
-            logger.error(error);
-          }
-        }}
+        onClick={() => onClickInvoked(page._id)}
       >
         <div className="d-flex">
           {/* checkbox */}
-          <div className="form-check my-auto mx-2">
+          <div className="form-check my-auto mr-3">
             <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
           </div>
           <div className="w-100">
@@ -70,30 +112,21 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
               <div className="d-flex mx-2">
                 <PageListMeta page={page} />
               </div>
-              {/* doropdown icon */}
+              {/* doropdown icon includes page control buttons */}
               <div className="ml-auto">
-                <i className="fa fa-ellipsis-v text-muted"></i>
+                <PageItemControl page={page} />
               </div>
-
-              {/* Todo: add the following icon into dropdown menu */}
-              {/* <button
-                type="button"
-                className="btn btn-link p-0"
-                value={page.path}
-                onClick={(e) => {
-                  window.location.href = e.currentTarget.value;
-                }}
-              >
-                <i className="icon-login" />
-              </button> */}
-
             </div>
-            {/* eslint-disable-next-line react/no-danger */}
-            <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
           </div>
         </div>
+        {/* TODO: adjust snippet position */}
+        {page.snippet
+          ? <div className="mt-1">page.snippet</div>
+          : <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
+        }
       </a>
     </li>
   );
 };
+
 export default SearchResultListItem;

+ 12 - 5
packages/app/src/interfaces/page.ts

@@ -5,10 +5,17 @@ import { ITag } from './tag';
 export type IPage = {
   path: string,
   status: string,
-  revision: IRevision,
-  tags: ITag[],
-  creator: IUser,
+  revision: string | IRevision,
+  tags?: ITag[],
+  lastUpdateUser: any,
+  commentCount: number,
+  creator: string | IUser,
+  seenUsers: string[],
+  liker: string[],
   createdAt: Date,
   updatedAt: Date,
-  seenUsers: string[]
-}
+};
+
+export type IPageHasId = IPage & {
+  _id: string,
+};

+ 5 - 0
packages/app/src/interfaces/search.ts

@@ -0,0 +1,5 @@
+export enum CheckboxType {
+  NONE_CHECKED = 'noneChecked',
+  INDETERMINATE = 'indeterminate',
+  ALL_CHECKED = 'allChecked',
+}

+ 1 - 1
packages/app/src/server/models/editor-settings.ts

@@ -32,7 +32,7 @@ const textlintSettingsSchema = new Schema<ITextlintSettings>({
   },
 });
 
-const editorSettingsSchema = new Schema<IEditorSettings>({
+const editorSettingsSchema = new Schema<EditorSettingsDocument, EditorSettingsModel>({
   userId: { type: String },
   textlintSettings: textlintSettingsSchema,
 });

+ 8 - 7
packages/app/src/server/routes/apiv3/page.js

@@ -176,6 +176,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('bool').isBoolean(),
     ],
+    info: [
+      query('pageId').isMongoId().withMessage('pageId is required'),
+    ],
     export: [
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
@@ -278,10 +281,10 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.get(('/info', loginRequired), async(req, res) => {
+  router.get('/info', loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
+    const { pageId } = req.query;
 
     try {
-      const pageId = req.query._id;
       const page = await Page.findById(pageId);
 
       const guestUserResponse = {
@@ -292,11 +295,9 @@ module.exports = (crowi) => {
         isSeen: page.seenUsers.length > 0,
       };
 
-      {
-        const isGuestUser = !req.user;
-        if (isGuestUser) {
-          return res.apiv3(guestUserResponse);
-        }
+      const isGuestUser = !req.user;
+      if (isGuestUser) {
+        return res.apiv3(guestUserResponse);
       }
 
       const userResponse = { ...guestUserResponse, isLiked: page.isLiked(req.user) };

+ 4 - 1
packages/app/src/styles/_layout.scss

@@ -99,11 +99,14 @@ body.growi-layout-fluid .grw-container-convertible {
 
 // printable style
 @media print {
-  padding: 30px;
+  body {
+    padding: 30px;
+  }
 
   a:after {
     display: none !important;
   }
+
   .main {
     header {
       border-bottom: solid 1px $secondary;

+ 2 - 2
packages/app/src/styles/_search.scss

@@ -163,10 +163,10 @@
     overflow-y: scroll;
 
     .nav.nav-pills {
-      > li {
+      > .page-list-li {
         > a {
           height: 123px;
-          padding: 2px 8px;
+          padding: 2px 4px;
           word-break: break-all;
           border-radius: 0;
 

+ 5 - 9
packages/app/src/styles/theme/_apply-colors.scss

@@ -17,6 +17,8 @@ $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcol
 $color-seen-user: #549c79 !default;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
+$bordercolor-search-item-left-active: $primary;
+$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%);
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -591,16 +593,10 @@ body.pathname-sidebar {
         background-color: $bgcolor-keyword-highlighted;
       }
       .page-list-ul {
-        > li.nav-item > a.nav-link {
-          color: inherit;
-        }
-        a {
-          &.hover {
-            background-color: darken($bgcolor-global, 4%);
-          }
+        .page-list-li {
           &.active {
-            background-color: darken($bgcolor-global, 8%);
-            border-color: theme-color('primary');
+            background-color: $bgcolor-search-item-active;
+            border-color: $bordercolor-search-item-left-active;
           }
         }
       }

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

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

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

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

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

+ 2 - 2
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
   },
   "dependencies": {
     "@slack/oauth": "^2.0.1",
-    "axios": "^0.21.1",
+    "axios": "^0.24.0",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "extensible-custom-error": "^0.0.7",

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "4.4.10-slackbot-proxy.0",
+  "version": "4.4.11-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.4.10-RC.0",
+    "@growi/slack": "^4.4.11-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
@@ -33,7 +33,7 @@
     "@tsed/platform-express": "^6.43.0",
     "@tsed/swagger": "^6.43.0",
     "@tsed/typeorm": "^6.43.0",
-    "axios": "^0.21.1",
+    "axios": "^0.24.0",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "compression": "^1.7.4",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "4.4.10-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

File diff suppressed because it is too large
+ 158 - 368
yarn.lock


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