Procházet zdrojové kódy

Merge branch 'support/share-link-for-outside-for-merge' into support/create-link-test

itizawa před 5 roky
rodič
revize
3804c71228

+ 21 - 1
CHANGES.md

@@ -1,9 +1,29 @@
 # CHANGES
 
-## v4.0.5
+## v4.0.7-RC
 
 * 
 
+## v4.0.6
+
+* Fix: Avatar images in Recent Changes are not shown
+* Fix: Full screen modal of Handsontable and Draw.io don't work
+* Fix: Shortcut for creating page respond with modifier key wrongly
+    * Introduced by v4.0.5
+
+## v4.0.5
+
+* Improvement: Return pre-defined session id when healthcheck
+* Improvement: Refactor caching for profile image
+* Improvement: Layout for global search help on mobile
+* Improvement: Layout for confidential notation
+* Fix: Shortcut for creating page doesn't work
+* Support: Dev in container
+* Support: Upgrade libs
+    * ldapjs
+    * node-sass
+
+
 ## v4.0.4
 
 * Feature: Drawer/Dock mode selector

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.0.5-RC",
+  "version": "4.0.7-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -211,7 +211,7 @@
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
-    "node-sass": "^4.12.0",
+    "node-sass": "^4.14.1",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",

+ 6 - 3
resource/locales/en-US/translation.json

@@ -339,11 +339,14 @@
   "toaster": {
     "update_successed": "Succeeded to update {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
-    "remove_user_admin": "Succeeded to remove {{username}} admin ",
+    "remove_user_admin": "Succeeded to remove {{username}} admin",
     "activate_user_success": "Succeeded to activating {{username}}",
     "deactivate_user_success": "Succeeded to deactivate {{username}}",
-    "remove_user_success": "Succeeded to removing {{username}} ",
-    "remove_external_user_success": "Succeeded to remove {{accountId}} "
+    "remove_user_success": "Succeeded to removing {{username}}",
+    "remove_external_user_success": "Succeeded to remove {{accountId}}",
+    "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
+    "issue_share_link": "Succeeded to issue new share link",
+    "remove_share_link": "Succeeded to remove {{count}} share links"
   },
   "template": {
     "modal_label": {

+ 4 - 1
resource/locales/ja/translation.json

@@ -344,7 +344,10 @@
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました"
+    "remove_external_user_success": "{{accountId}}を削除しました",
+    "remove_share_link_success": "{{shareLinkId}}を削除しました",
+    "issue_share_link": "共有リンクを作成しました",
+    "remove_share_link": "共有リンクを{{count}}件削除しました"
   },
   "template": {
     "modal_label": {

+ 5 - 0
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -18,6 +18,7 @@ import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import TwitterSecuritySetting from './TwitterSecuritySetting';
 import FacebookSecuritySetting from './FacebookSecuritySetting';
+import ShareLinkSetting from './ShareLinkSetting';
 
 class SecurityManagement extends React.Component {
 
@@ -47,6 +48,10 @@ class SecurityManagement extends React.Component {
         <div>
           <SecuritySetting />
         </div>
+        <div>
+          <ShareLinkSetting />
+        </div>
+
 
         {/* XSS configuration link */}
         <div className="mb-5">

+ 48 - 0
src/client/js/components/Admin/Security/ShareLinkSetting.jsx

@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+
+class ShareLinkSetting extends React.Component {
+
+  render() {
+    return (
+      <div className="container">
+        <div className="mb-3">
+          <h2 className="alert-anchor border-bottom">Shared Link List</h2>
+        </div>
+        <button className="pull-right btn btn-danger" type="button">Delete all links</button>
+
+        <div className="table-responsive">
+          <table className="table table-bordered">
+            <thead>
+              <tr>
+                <th>Link</th>
+                <th>PagePath</th>
+                <th>Expiration</th>
+                <th>Description</th>
+                <th>Order</th>
+              </tr>
+            </thead>
+            <tbody>
+              {/* ShareLinkListを参考に */}
+            </tbody>
+          </table>
+        </div>
+
+      </div>
+    );
+  }
+
+}
+
+const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSetting, [AdminGeneralSecurityContainer]);
+
+ShareLinkSetting.propTypes = {
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+};
+
+export default withTranslation()(ShareLinkSettingWrapper);

+ 77 - 30
src/client/js/components/OutsideShareLinkModal.jsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import {
@@ -15,40 +15,87 @@ import PageContainer from '../services/PageContainer';
 import ShareLinkList from './ShareLinkList';
 import ShareLinkForm from './ShareLinkForm';
 
-const OutsideShareLinkModal = (props) => {
-  const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState(false);
+import { toastSuccess, toastError } from '../util/apiNotification';
 
-  function toggleShareLinkFormHandler() {
-    setIsOpenShareLinkForm(!isOpenShareLinkForm);
+class OutsideShareLinkModal extends React.Component {
+
+  constructor() {
+    super();
+    this.state = {
+      shareLinks: [],
+      isOpenShareLinkForm: false,
+    };
+
+    this.toggleShareLinkFormHandler = this.toggleShareLinkFormHandler.bind(this);
+    this.deleteAllLinksButtonHandler = this.deleteAllLinksButtonHandler.bind(this);
+    this.deleteLinkById = this.deleteLinkById.bind(this);
   }
 
-  return (
-    <Modal size="lg" isOpen={props.isOpen} toggle={props.onClose}>
-      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">Title
-      </ModalHeader>
-      <ModalBody>
-        <div className="container">
-          <div className="form-inline mb-3">
-            <h4>Shared Link List</h4>
-            <button className="ml-auto btn btn-danger" type="button">Delete all links</button>
-          </div>
+  toggleShareLinkFormHandler() {
+    this.setState({ isOpenShareLinkForm: !this.state.isOpenShareLinkForm });
+  }
+
+  async deleteAllLinksButtonHandler() {
+    const { t, appContainer, pageContainer } = this.props;
+    const { pageId } = pageContainer.state;
+
+    try {
+      const res = await appContainer.apiv3.delete('/share-links/', { relatedPage: pageId });
+      const count = res.data.n;
+      toastSuccess(t('toaster.remove_share_link', { count }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+    // TODO GW-2764 retrieve share links
+  }
 
-          <div>
-            <ShareLinkList />
-            <button
-              className="btn btn-outline-secondary d-block mx-auto px-5 mb-3"
-              type="button"
-              onClick={toggleShareLinkFormHandler}
-            >
-              {isOpenShareLinkForm ? 'Close' : 'New'}
-            </button>
-            {isOpenShareLinkForm && <ShareLinkForm />}
+  async deleteLinkById(shareLinkId) {
+    const { t, appContainer } = this.props;
+
+    try {
+      const res = await appContainer.apiv3Delete(`/share-links/${shareLinkId}`);
+      const { deletedShareLink } = res.data;
+      toastSuccess(t('remove_share_link_success', { shareLinkId: deletedShareLink._id }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+    // TODO GW-2764 retrieve share links
+  }
+
+  render() {
+    return (
+      <Modal size="lg" isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-primary text-light">Title
+        </ModalHeader>
+        <ModalBody>
+          <div className="container">
+            <div className="form-inline mb-3">
+              <h4>Shared Link List</h4>
+              <button className="ml-auto btn btn-danger" type="button" onClick={this.deleteAllLinksButtonHandler}>Delete all links</button>
+            </div>
+
+            <div>
+              <ShareLinkList shareLinks={this.state.shareLinks} />
+              <button
+                className="btn btn-outline-secondary d-block mx-auto px-5 mb-3"
+                type="button"
+                onClick={this.toggleShareLinkFormHandler}
+              >
+                {this.state.isOpenShareLinkForm ? 'Close' : 'New'}
+              </button>
+              {this.state.isOpenShareLinkForm && <ShareLinkForm onCloseForm={this.toggleShareLinkFormHandler} onClickDeleteButton={this.deleteLinkById} />}
+            </div>
           </div>
-        </div>
-      </ModalBody>
-    </Modal>
-  );
-};
+        </ModalBody>
+      </Modal>
+    );
+  }
+
+}
 
 /**
  * Wrapper component for using unstated

+ 68 - 4
src/client/js/components/ShareLinkForm.jsx

@@ -1,10 +1,14 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
+import parse from 'date-fns/parse';
 
 import { withUnstatedContainers } from './UnstatedUtils';
 
+import { toastSuccess, toastError } from '../util/apiNotification';
+
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 
@@ -66,10 +70,63 @@ class ShareLinkForm extends React.Component {
     this.setState({ customExpirationTime });
   }
 
-  handleIssueShareLink() {
-    // use these options
-    console.log(this.state);
-    console.log('発行する!');
+  /**
+   * Generate expiredAt by expirationType
+   */
+  generateExpired() {
+    const { expirationType } = this.state;
+    let expiredAt;
+
+    if (expirationType === 'unlimited') {
+      return null;
+    }
+
+    if (expirationType === 'numberOfDays') {
+      const date = new Date();
+      date.setDate(date.getDate() + this.state.numberOfDays);
+      expiredAt = date;
+    }
+
+    if (expirationType === 'custom') {
+      const { customExpirationDate, customExpirationTime } = this.state;
+      expiredAt = parse(`${customExpirationDate}T${customExpirationTime}`, "yyyy-MM-dd'T'HH:mm:ss", new Date());
+    }
+
+    return expiredAt;
+  }
+
+  closeForm() {
+    const { onCloseForm } = this.props;
+
+    if (onCloseForm == null) {
+      return;
+    }
+    onCloseForm();
+  }
+
+  async handleIssueShareLink() {
+    const { t, appContainer, pageContainer } = this.props;
+    const { pageId } = pageContainer.state;
+    const { description } = this.state;
+
+    let expiredAt;
+
+    try {
+      expiredAt = this.generateExpired();
+    }
+    catch (err) {
+      return toastError(err);
+    }
+
+    try {
+      await appContainer.apiv3Post('/share-links/', { relatedPage: pageId, expiredAt, description });
+      this.closeForm();
+      toastSuccess(t('toaster.issue_share_link'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
   }
 
   renderExpirationTypeOptions() {
@@ -188,5 +245,12 @@ class ShareLinkForm extends React.Component {
 
 const ShareLinkFormWrapper = withUnstatedContainers(ShareLinkForm, [AppContainer, PageContainer]);
 
+ShareLinkForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  onCloseForm: PropTypes.func,
+};
 
 export default withTranslation()(ShareLinkFormWrapper);

+ 18 - 32
src/client/js/components/ShareLinkList.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
-import * as toastr from 'toastr';
+import PropTypes from 'prop-types';
+
 
 import { withTranslation } from 'react-i18next';
 
@@ -9,46 +10,23 @@ import AppContainer from '../services/AppContainer';
 
 const ShareLinkList = (props) => {
 
-  function deleteLinkHandler(shareLink) {
-    try {
-      // call api
-      toastr.success(`Successfully deleted ${shareLink._id}`);
-    }
-    catch (err) {
-      toastr.error(new Error(`Failed to delete ${shareLink._id}`));
+  function deleteLinkHandler(shareLinkId) {
+    if (props.onClickDeleteButton == null) {
+      return;
     }
+    props.onClickDeleteButton(shareLinkId);
   }
 
-  function GetShareLinkList() {
-    // dummy data
-    const dummyDate = new Date().toString();
-    const shareLinks = [
-      {
-        _id: '507f1f77bcf86cd799439011', link: '/507f1f77bcf86cd799439011', expiration: dummyDate, description: 'foobar',
-      },
-      {
-        _id: '52fcebd19a5c4ea066dbfa12', link: '/52fcebd19a5c4ea066dbfa12', expiration: dummyDate, description: 'test',
-      },
-      {
-        _id: '54759eb3c090d83494e2d804', link: '/54759eb3c090d83494e2d804', expiration: dummyDate, description: 'hoge',
-      },
-      {
-        _id: '5349b4ddd2781d08c09890f3', link: '/5349b4ddd2781d08c09890f3', expiration: dummyDate, description: 'fuga',
-      },
-      {
-        _id: '5349b4ddd2781d08c09890f4', link: '/5349b4ddd2781d08c09890f4', expiration: dummyDate, description: 'piyo',
-      },
-    ];
-
+  function renderShareLinks() {
     return (
       <>
-        {shareLinks.map(shareLink => (
+        {props.shareLinks.map(shareLink => (
           <tr>
             <td>{shareLink.link}</td>
             <td>{shareLink.expiration}</td>
             <td>{shareLink.description}</td>
             <td>
-              <button className="btn btn-outline-warning" type="button" onClick={() => deleteLinkHandler(shareLink)}>
+              <button className="btn btn-outline-warning" type="button" onClick={() => deleteLinkHandler(shareLink._id)}>
                 <i className="icon-trash"></i>Delete
               </button>
             </td>
@@ -70,7 +48,7 @@ const ShareLinkList = (props) => {
           </tr>
         </thead>
         <tbody>
-          <GetShareLinkList />
+          {renderShareLinks()}
         </tbody>
       </table>
     </div>
@@ -79,4 +57,12 @@ const ShareLinkList = (props) => {
 
 const ShareLinkListWrapper = withUnstatedContainers(ShareLinkList, [AppContainer]);
 
+ShareLinkList.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  shareLinks: PropTypes.array.isRequired,
+  onClickDeleteButton: PropTypes.func,
+};
+
 export default withTranslation()(ShareLinkListWrapper);

+ 1 - 1
src/client/js/components/Sidebar/RecentChanges.jsx

@@ -59,7 +59,7 @@ class RecentChanges extends React.Component {
     return (
       <li className="list-group-item p-2">
         <div className="d-flex w-100">
-          <UserPicture user={page.lastUpdatedUser} size="md" />
+          <UserPicture user={page.lastUpdateUser} size="md" />
           <div className="flex-grow-1 ml-2">
             { !dPagePath.isRoot && <FormerLink /> }
             <h5 className="mb-1">

+ 4 - 1
src/client/js/services/AppContainer.js

@@ -117,7 +117,10 @@ export default class AppContainer extends Container {
       }
 
       if (event.key === 'c') {
-        this.setState({ isPageCreateModalShown: true });
+        // don't fire when not needed
+        if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
+          this.setState({ isPageCreateModalShown: true });
+        }
       }
     });
   }

+ 2 - 2
src/client/styles/scss/_mixins.scss

@@ -83,9 +83,9 @@
 @mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
   // full-screen modal
   width: auto;
-  max-width: unset;
+  max-width: unset !important;
   height: calc(100vh - 30px);
-  margin: 15px;
+  margin: 15px !important;
 
   .modal-content {
     height: calc(100vh - 30px);

+ 1 - 1
src/lib/util/mongoose-utils.js

@@ -7,7 +7,7 @@ const getMongoUri = () => {
     || env.MONGODB_URI // MONGOLAB changes their env name
     || env.MONGOHQ_URL
     || env.MONGO_URI
-    || ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
+    || ((env.NODE_ENV === 'test') ? 'mongodb://mongo/growi_test' : 'mongodb://mongo/growi');
 };
 
 const getModelSafely = (modelName) => {

+ 8 - 0
src/server/models/share-link.js

@@ -25,6 +25,14 @@ schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 
 module.exports = function(crowi) {
+
+  schema.methods.isExpired = function() {
+    if (this.expiredAt == null) {
+      return false;
+    }
+    return this.expiredAt.getTime() < new Date().getTime();
+  };
+
   const model = mongoose.model('ShareLink', schema);
   return model;
 };

+ 26 - 0
src/server/routes/apiv3/security-setting.js

@@ -588,6 +588,32 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/all-share-links:
+   *      delete:
+   *        tags: [ShareLinkSettings, apiv3]
+   *        description: Delete All ShareLinks at Share Link Setting
+   *        responses:
+   *          200:
+   *            description: succeed to delete all share links
+   */
+
+  router.delete('/all-share-links/', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const ShareLink = crowi.model('ShareLink');
+    try {
+      const removedAct = await ShareLink.remove({});
+      const removeTotal = await removedAct.n;
+      return res.apiv3({ removeTotal });
+    }
+    catch (err) {
+      const msg = 'Error occured in delete all share links';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'failed-to-delete-all-share-links'));
+    }
+  });
+
   /**
    * @swagger
    *

+ 10 - 15
src/server/routes/apiv3/share-links.js

@@ -64,13 +64,10 @@ module.exports = (crowi) => {
   validator.shareLinkStatus = [
     // validate the page id is null
     body('relatedPage').not().isEmpty().withMessage('Page Id is null'),
-
     // validate expireation date is not empty, is not before today and is date.
-    body('expiredAt').isAfter(today.toString()).withMessage('Your Selected date is past'),
-
+    body('expiredAt').if(value => value != null).isAfter(today.toString()).withMessage('Your Selected date is past'),
     // validate the length of description is max 100.
     body('description').isLength({ min: 0, max: 100 }).withMessage('Max length is 100'),
-
   ];
 
   /**
@@ -126,21 +123,19 @@ module.exports = (crowi) => {
   *        tags: [ShareLinks]
   *        summary: /share-links/
   *        description: delete all share links related one page
-  *        requestBody:
-  *           required: true
-  *           content:
-  *             application/json:
-  *               schema:
-  *                 properties:
-  *                   relatedPage:
-  *                     type: string
-  *                     description: delete all share links that related one page
+  *        parameters:
+  *          - name: relatedPage
+  *            in: query
+  *            required: true
+  *            description: page id of share link
+  *            schema:
+  *              type: string
   *        responses:
   *          200:
   *            description: Succeeded to delete o all share links related one page
   */
   router.delete('/', loginRequired, csrf, async(req, res) => {
-    const { relatedPage } = req.body;
+    const { relatedPage } = req.query;
     const ShareLink = crowi.model('ShareLink');
 
     try {
@@ -177,7 +172,7 @@ module.exports = (crowi) => {
 
     try {
       const deletedShareLink = await ShareLink.findOneAndRemove({ _id: id });
-      return res.apiv3(deletedShareLink);
+      return res.apiv3({ deletedShareLink });
     }
     catch (err) {
       const msg = 'Error occurred in delete share link';

+ 5 - 4
src/server/routes/page.js

@@ -449,15 +449,16 @@ module.exports = function(crowi, app) {
     const view = `layout-${layoutName}/shared_page`;
 
     const shareLink = await ShareLink.findOne({ _id: linkId }).populate('relatedPage');
-    let page = shareLink.relatedPage;
 
-    if (page == null) {
-      // page is not found
+    if (shareLink == null || shareLink.relatedPage == null) {
+      // page or sharelink are not found
       return res.render(`layout-${layoutName}/not_found_shared_page`);
     }
 
+    let page = shareLink.relatedPage;
+
     // check if share link is expired
-    if (shareLink.expiredAt.getTime() < new Date().getTime()) {
+    if (shareLink.isExpired()) {
       // page is not found
       return res.render(`layout-${layoutName}/expired_shared_page`);
     }

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 205 - 105
yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů