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

Merge remote-tracking branch 'origin/feat/article-area-renovation' into feat/GW-3662-switch-file-uploader-form

itizawa 5 лет назад
Родитель
Сommit
e33970e20b

+ 2 - 0
resource/locales/en_US/translation.json

@@ -1,5 +1,6 @@
 {
   "Help": "Help",
+  "view": "View",
   "Edit": "Edit",
   "Delete": "Delete",
   "delete_all": "Delete all",
@@ -428,6 +429,7 @@
     "open_sandbox": "Open Sandbox"
   },
   "hackmd": {
+    "hack_md": "HackMD",
     "not_set_up": "HackMD is not set up.",
     "start_to_edit": "Start to edit with HackMD",
     "clone_page_content": "Click to clone page content and start to edit.",

+ 2 - 0
resource/locales/ja_JP/translation.json

@@ -1,5 +1,6 @@
 {
   "Help": "ヘルプ",
+  "view": "View",
   "Edit": "編集",
   "Delete": "削除",
   "delete_all": "全て削除",
@@ -430,6 +431,7 @@
     "open_sandbox": "Sandbox を開く"
   },
   "hackmd":{
+    "hack_md": "HackMD",
     "not_set_up": "HackMD はセットアップされていません",
     "start_to_edit": "HackMD を開始する",
     "clone_page_content": "ページを複製して編集を開始します",

+ 3 - 1
resource/locales/zh_CN/translation.json

@@ -1,5 +1,6 @@
 {
-	"Help": "帮助",
+  "Help": "帮助",
+  "view": "View",
 	"Edit": "编辑",
 	"Delete": "删除",
 	"delete_all": "删除所有",
@@ -404,6 +405,7 @@
 		"open_sandbox": "开放式沙箱"
 	},
 	"hackmd": {
+    "hack_md": "HackMD",
 		"not_set_up": "HackMD is not set up.",
 		"start_to_edit": "Start to edit with HackMD",
 		"clone_page_content": "Click to clone page content and start to edit.",

+ 15 - 9
src/client/js/components/BookmarkButton.jsx

@@ -10,6 +10,7 @@ class BookmarkButton extends React.Component {
 
     this.state = {
       isBookmarked: false,
+      sumOfBookmarks: 0,
     };
 
     this.handleClick = this.handleClick.bind(this);
@@ -28,6 +29,8 @@ class BookmarkButton extends React.Component {
       if (response.data.bookmark != null) {
         this.setState({ isBookmarked: true });
       }
+      const result = await crowi.apiv3.get('/bookmarks/count-bookmarks', { pageId });
+      this.setState({ sumOfBookmarks: result.data.sumOfBookmarks });
     }
     catch (err) {
       toastError(err);
@@ -59,17 +62,20 @@ class BookmarkButton extends React.Component {
     }
 
     return (
-      <button
-        type="button"
-        href="#"
-        title="Bookmark"
-        onClick={this.handleClick}
-        className={`btn rounded-circle btn-bookmark border-0 d-edit-none
+      <div className="d-flex">
+        <button
+          type="button"
+          onClick={this.handleClick}
+          className={`btn rounded-circle btn-bookmark border-0 d-edit-none
           ${`btn-${this.props.size}`}
           ${this.state.isBookmarked ? 'active' : ''}`}
-      >
-        <i className="icon-star"></i>
-      </button>
+        >
+          <i className="icon-star"></i>
+        </button>
+        <div className="total-bookmarks">
+          {this.state.sumOfBookmarks}
+        </div>
+      </div>
     );
   }
 

+ 11 - 10
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -18,6 +18,7 @@ import RevisionPathControls from '../Page/RevisionPathControls';
 import TagLabels from '../Page/TagLabels';
 import LikeButton from '../LikeButton';
 import BookmarkButton from '../BookmarkButton';
+import ThreeStrandedButton from './ThreeStrandedButton';
 
 import PageCreator from './PageCreator';
 import RevisionAuthor from './RevisionAuthor';
@@ -107,15 +108,20 @@ const UserInfo = ({ pageUser }) => {
 /* eslint-disable react/prop-types */
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 
-  const { pageId, isLiked, pageUser } = pageContainer.state;
+  const {
+    pageId, isLiked, pageUser, sumOfLikers,
+  } = pageContainer.state;
 
   return (
     <>
       {pageUser == null && (
-      <span className="mr-2">
+      <span>
         <LikeButton pageId={pageId} isLiked={isLiked} />
       </span>
       )}
+      <span className="mr-2 total-likes">
+        {sumOfLikers}
+      </span>
       <span className="mr-2">
         <BookmarkButton pageId={pageId} crowi={appContainer} />
       </span>
@@ -186,14 +192,9 @@ const GrowiSubNavigation = (props) => {
         <div className="d-flex flex-column align-items-end justify-content-center">
           <div className="d-flex">
             { !isPageInTrash && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
-            <div className="mt-2">
-              {/* TODO: impl View / Edit / HackMD button group */}
-              {/* <div className="btn-group" role="group" aria-label="Basic example">
-              <button type="button" className="btn btn-outline-primary">Left</button>
-              <button type="button" className="btn btn-outline-primary">Middle</button>
-              <button type="button" className="btn btn-outline-primary">Right</button>
-            </div> */}
-            </div>
+          </div>
+          <div className="mt-2">
+            <ThreeStrandedButton />
           </div>
         </div>
 

+ 39 - 0
src/client/js/components/Navbar/ThreeStrandedButton.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+const ThreeStrandedButton = (props) => {
+
+  const { t } = props;
+
+  function threeStrandedButtonClickedHandler(viewType) {
+    if (props.onThreeStrandedButtonClicked != null) {
+      props.onThreeStrandedButtonClicked(viewType);
+    }
+  }
+
+  return (
+    <div className="btn-group grw-three-stranded-button" role="group " aria-label="three-stranded-button">
+      <button type="button" className="btn btn-outline-primary view-button" onClick={() => { threeStrandedButtonClickedHandler('view') }}>
+        <i className="icon-control-play icon-fw" />
+        { t('view') }
+      </button>
+      <button type="button" className="btn btn-outline-primary edit-button" onClick={() => { threeStrandedButtonClickedHandler('edit') }}>
+        <i className="icon-note icon-fw" />
+        { t('Edit') }
+      </button>
+      <button type="button" className="btn btn-outline-primary hackmd-button" onClick={() => { threeStrandedButtonClickedHandler('hackmd') }}>
+        <i className="fa fa-fw fa-file-text-o" />
+        { t('hackmd.hack_md') }
+      </button>
+    </div>
+  );
+
+};
+
+ThreeStrandedButton.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  onThreeStrandedButtonClicked: PropTypes.func,
+};
+
+export default withTranslation()(ThreeStrandedButton);

+ 26 - 0
src/client/styles/scss/_subnav.scss

@@ -39,6 +39,20 @@
     font-size: 20px;
   }
 
+  .total-likes {
+    width: 8px;
+    height: 16px;
+    padding: 0.5em 0 0 0;
+    font-size: 16px;
+  }
+
+  .total-bookmarks {
+    width: 8px;
+    height: 16px;
+    padding: 0.5em 0 0 0;
+    font-size: 16px;
+  }
+
   ul.authors {
     padding: 0.7em 0 0.7em 1.5em;
     margin-bottom: 0;
@@ -83,6 +97,18 @@
       height: 30px;
       font-size: 15px !important;
     }
+
+    .total-likes {
+      width: 6px;
+      height: 12px;
+      font-size: 12px;
+    }
+
+    .total-bookmarks {
+      width: 6px;
+      height: 12px;
+      font-size: 12px;
+    }
   }
 }
 

+ 9 - 0
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -214,6 +214,15 @@ ul.pagination {
   background-color: rgba($bgcolor-subnav, 0.85);
 }
 
+.grw-three-stranded-button {
+  .btn-outline-primary {
+    &:hover {
+      color: $primary;
+      background-color: $gray-700;
+    }
+  }
+}
+
 // Search drop down
 #search-typeahead-asynctypeahead {
   background-color: $bgcolor-global;

+ 9 - 0
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -132,6 +132,15 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: rgba($bgcolor-subnav, 0.85);
 }
 
+.grw-three-stranded-button {
+  .btn-outline-primary {
+    &:hover {
+      color: $primary;
+      background-color: $gray-200;
+    }
+  }
+}
+
 .grw-drawer-toggler {
   @extend .btn-light;
   color: $gray-500;

+ 45 - 4
src/server/routes/apiv3/bookmarks.js

@@ -3,7 +3,7 @@ const loggerFactory = require('@alias/logger');
 const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-line no-unused-vars
 
 const express = require('express');
-const { body } = require('express-validator');
+const { body, query } = require('express-validator');
 
 const router = express.Router();
 
@@ -54,7 +54,8 @@ const router = express.Router();
 
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequired = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
@@ -62,9 +63,12 @@ module.exports = (crowi) => {
 
   const validator = {
     bookmarks: [
-      body('pageId').isString(),
+      body('pageId').isMongoId(),
       body('bool').isBoolean(),
     ],
+    countBookmarks: [
+      query('pageId').isMongoId(),
+    ],
   };
 
   /**
@@ -126,7 +130,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Bookmark'
    */
-  router.put('/', accessTokenParser, loginRequired, csrf, validator.bookmarks, apiV3FormValidator, async(req, res) => {
+  router.put('/', accessTokenParser, loginRequiredStrictly, csrf, validator.bookmarks, apiV3FormValidator, async(req, res) => {
     const { pageId, bool } = req.body;
 
     let bookmark;
@@ -153,5 +157,42 @@ module.exports = (crowi) => {
     return res.apiv3({ bookmark });
   });
 
+  /**
+   * @swagger
+   *
+   *    /count-bookmarks:
+   *      get:
+   *        tags: [Bookmarks]
+   *        summary: /bookmarks
+   *        description: Count bookmsrks
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/BookmarkParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to count bookmarks.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Bookmark'
+   */
+
+
+  router.get('/count-bookmarks', accessTokenParser, loginRequired, validator.countBookmarks, apiV3FormValidator, async(req, res) => {
+    const { pageId } = req.query;
+
+    try {
+      const sumOfBookmarks = await Bookmark.countByPageId(pageId);
+      return res.apiv3({ sumOfBookmarks });
+    }
+    catch (err) {
+      logger.error('get-bookmarks-list-failed', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+
   return router;
 };