Browse Source

Merge branch 'support/share-link-for-outside-for-merge' into imprv/descard-link

# Conflicts:
#	src/client/js/components/OutsideShareLinkModal.jsx
#	src/client/styles/scss/_sharelink.scss
#	src/client/styles/scss/style-app.scss
ryohek 5 years ago
parent
commit
92167f1adf

+ 6 - 110
src/client/js/components/OutsideShareLinkModal.jsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import * as toastr from 'toastr';
 
 import {
   Modal, ModalHeader, ModalBody,
@@ -13,57 +12,12 @@ import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 
+import ShareLinkList from './ShareLinkList';
+import ShareLinkForm from './ShareLinkForm';
+
 const OutsideShareLinkModal = (props) => {
 
   /* const { t } = props; */
-  // 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 deleteLinkHandler(shareLink) {
-    try {
-      // call api
-      toastr.success(`${shareLink._id} is deleted successfully`);
-    }
-    catch (err) {
-      toastr.error(new Error(`${shareLink._id} is deleted failed`));
-    }
-  }
-
-  function ShareLinkList() {
-    return (
-      <>
-        {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)}>
-                <i className="icon-trash"></i>Delete
-              </button>
-            </td>
-          </tr>
-        ))}
-      </>
-    );
-  }
 
   return (
     <Modal size="lg" isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
@@ -76,68 +30,10 @@ const OutsideShareLinkModal = (props) => {
             <button className="col btn btn-danger" type="button">Delete all links</button>
           </div>
 
-          <div className="">
-            <div className="table-responsive">
-              <table className="table table-bordered">
-                <thead>
-                  <tr>
-                    <th>Link</th>
-                    <th>Expiration</th>
-                    <th>Description</th>
-                    <th>Order</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <ShareLinkList />
-                </tbody>
-              </table>
-            </div>
+          <div>
+            <ShareLinkList />
             <button className="btn btn-outline-secondary d-block mx-auto px-5 mb-3" type="button">+</button>
-
-            <div className="share-link-form border">
-              <h4 className="ml-3">Expiration Date</h4>
-              <form>
-                <div className="form-group">
-                  <div className="custom-control custom-radio offset-4 mb-2">
-                    <input id="customRadio1" name="customRadio" type="radio" className="custom-control-input"></input>
-                    <label className="custom-control-label" htmlFor="customRadio1">Unlimited</label>
-                  </div>
-
-                  <div className="custom-control custom-radio offset-4 mb-2">
-                    <input id="customRadio2" name="customRadio" type="radio" className="custom-control-input"></input>
-                    <label className="custom-control-label" htmlFor="customRadio2">
-                      <div className="row align-items-center m-0">
-                        <input className="form-control col-2" type="number" min="1" max="7" value="7"></input>
-                        <span className="col-auto">Days</span>
-                      </div>
-                    </label>
-                  </div>
-
-                  <div className="custom-control custom-radio offset-4 mb-2">
-                    <input id="customRadio3" name="customRadio" type="radio" className="custom-control-input"></input>
-                    <label className="custom-control-label" htmlFor="customRadio3">
-                      Custom
-                      <div className="date-picker">Date Picker</div>
-                    </label>
-                  </div>
-
-                  <hr />
-
-                  <div className="form-group row">
-                    <label htmlFor="inputDesc" className="col-md-4 col-form-label">Description</label>
-                    <div className="col-md-4">
-                      <input type="text" className="form-control" id="inputDesc" placeholder="Enter description"></input>
-                    </div>
-                  </div>
-
-                  <div className="form-group row">
-                    <div className="offset-8 col">
-                      <button type="button" className="btn btn-primary">Issue</button>
-                    </div>
-                  </div>
-                </div>
-              </form>
-            </div>
+            <ShareLinkForm />
           </div>
         </div>
       </ModalBody>

+ 64 - 0
src/client/js/components/ShareLinkForm.jsx

@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+
+const ShareLinkForm = (props) => {
+  return (
+    <div className="share-link-form border">
+      <h4 className="ml-3">Expiration Date</h4>
+      <form>
+        <div className="form-group">
+          <div className="custom-control custom-radio offset-4 mb-2">
+            <input id="customRadio1" name="customRadio" type="radio" className="custom-control-input"></input>
+            <label className="custom-control-label" htmlFor="customRadio1">Unlimited</label>
+          </div>
+
+          <div className="custom-control custom-radio offset-4 mb-2">
+            <input id="customRadio2" name="customRadio" type="radio" className="custom-control-input"></input>
+            <label className="custom-control-label" htmlFor="customRadio2">
+              <div className="row align-items-center m-0">
+                <input className="form-control col-2" type="number" min="1" max="7" value="7"></input>
+                <span className="col-auto">Days</span>
+              </div>
+            </label>
+          </div>
+
+          <div className="custom-control custom-radio offset-4 mb-2">
+            <input id="customRadio3" name="customRadio" type="radio" className="custom-control-input"></input>
+            <label className="custom-control-label" htmlFor="customRadio3">
+              Custom
+              <div className="date-picker">Date Picker</div>
+            </label>
+          </div>
+
+          <hr />
+
+          <div className="form-group row">
+            <label htmlFor="inputDesc" className="col-md-4 col-form-label">Description</label>
+            <div className="col-md-4">
+              <input type="text" className="form-control" id="inputDesc" placeholder="Enter description"></input>
+            </div>
+          </div>
+
+          <div className="form-group row">
+            <div className="offset-8 col">
+              <button type="button" className="btn btn-primary">Issue</button>
+            </div>
+          </div>
+        </div>
+      </form>
+    </div>
+  );
+};
+
+const ShareLinkFormWrapper = (props) => {
+  return createSubscribedElement(ShareLinkForm, props, [AppContainer, PageContainer]);
+};
+
+export default withTranslation()(ShareLinkFormWrapper);

+ 84 - 0
src/client/js/components/ShareLinkList.jsx

@@ -0,0 +1,84 @@
+import React from 'react';
+import * as toastr from 'toastr';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+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 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',
+      },
+    ];
+
+    return (
+      <>
+        {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)}>
+                <i className="icon-trash"></i>Delete
+              </button>
+            </td>
+          </tr>
+        ))}
+      </>
+    );
+  }
+
+  return (
+    <div className="table-responsive">
+      <table className="table table-bordered">
+        <thead>
+          <tr>
+            <th>Link</th>
+            <th>Expiration</th>
+            <th>Description</th>
+            <th>Order</th>
+          </tr>
+        </thead>
+        <tbody>
+          <GetShareLinkList />
+        </tbody>
+      </table>
+    </div>
+  );
+};
+
+const ShareLinkListWrapper = (props) => {
+  return createSubscribedElement(ShareLinkList, props, [AppContainer]);
+};
+
+export default withTranslation()(ShareLinkListWrapper);

+ 5 - 5
src/client/styles/scss/_search.scss

@@ -241,25 +241,25 @@
     th {
       text-align: right;
     }
-  
+
     td {
       overflow-wrap: anywhere;
       white-space: normal !important;
     }
-  
+
     @include media-breakpoint-down(xs) {
       th,
       td {
         display: block;
       }
-      
+
       th {
         text-align: left;
       }
-      
+
       td {
-        border-top: none !important;
         padding-top: 0 !important;
+        border-top: none !important;
       }
     }
   }

+ 12 - 0
src/client/styles/scss/_sharelink.scss

@@ -0,0 +1,12 @@
+.share-link-form {
+  /* Chrome/Safari */
+  input[type='number']::-webkit-outer-spin-button,
+  input[type='number']::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+  }
+
+  /* Firefox */
+  input[type='number'] {
+    -moz-appearance: textfield;
+  }
+}

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

@@ -59,6 +59,7 @@
 @import 'staff_credit';
 @import 'waves';
 @import 'wiki';
+@import 'sharelink';
 
 /*
  * for Guest User Mode

+ 1 - 1
src/server/models/share-link.js

@@ -16,7 +16,7 @@ const schema = new mongoose.Schema({
     required: true,
     index: true,
   },
-  expiration: { type: Date },
+  expiredAt: { type: Date },
   description: { type: String },
   createdAt: { type: Date, default: Date.now, required: true },
 });

+ 93 - 12
src/server/routes/apiv3/share-links.js

@@ -8,38 +8,119 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body, query } = require('express-validator/check');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
+const validator = {};
+
+const today = new Date();
+
 /**
  * @swagger
  *  tags:
- *    name: ShareLinks
+ *    name: ShareLink
  */
 
 module.exports = (crowi) => {
   const loginRequired = require('../../middleware/login-required')(crowi);
   const csrf = require('../../middleware/csrf')(crowi);
-
+  const { ApiV3FormValidator } = crowi.middlewares;
   const ShareLink = crowi.model('ShareLink');
 
-  // TDOO write swagger
-  router.get('/', loginRequired, async(req, res) => {
-    const { pageId } = req.query;
-    // TODO GW-2616 get all share links associated with the page
+
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /share-links/:
+   *      post:
+   *        tags: [ShareLink]
+   *        description: get share links
+   *        parameters:
+   *          - name: relatedPage
+   *            in: query
+   *            required: true
+   *            description: page id of share link
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to get share links
+   */
+  router.get('/', loginRequired, csrf, ApiV3FormValidator, async(req, res) => {
+    const { relatedPage } = req.query;
+    try {
+      const paginateResult = await ShareLink.find({ relatedPage: { $in: relatedPage } });
+      return res.apiv3({ paginateResult });
+    }
+    catch (err) {
+      const msg = 'Error occurred in get share link';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'get-shareLink-failed'));
+    }
   });
 
+  validator.shareLinkStatus = [
+    // validate the page id is null
+    body('relatedPage').not().isEmpty().withMessage('Page Id is null'),
 
-  // TDOO write swagger
-  router.post('/', loginRequired, async(req, res) => {
-    const { pageId } = req.body;
-    // TODO GW-2609 publish the share link
+    // validate expireation date is not empty, is not before today and is date.
+    body('expiredAt').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'),
+
+  ];
+
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /share-links/:
+   *      post:
+   *        tags: [ShareLink]
+   *        description: Create new share link
+   *        parameters:
+   *          - name: relatedPage
+   *            in: query
+   *            required: true
+   *            description: page id of share link
+   *            schema:
+   *              type: string
+   *          - name: expiredAt
+   *            in: query
+   *            description: expiration date of share link
+   *            schema:
+   *              type: string
+   *          - name: description
+   *            in: query
+   *            description: description of share link
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to create one share link
+   */
+
+  router.post('/', loginRequired, csrf, validator.shareLinkStatus, ApiV3FormValidator, async(req, res) => {
+    const { relatedPage, expiredAt, description } = req.body;
+    const ShareLink = crowi.model('ShareLink');
+
+    try {
+      const postedShareLink = await ShareLink.create({ relatedPage, expiredAt, description });
+      return res.apiv3(postedShareLink);
+    }
+    catch (err) {
+      const msg = 'Error occured in post share link';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'post-shareLink-failed'));
+    }
   });
 
   // TDOO write swagger
   router.delete('/all', loginRequired, async(req, res) => {
-    const { pageId } = req.body;
+    const { relatedPage } = req.body;
     // TODO GW-2694 Delete all share links
   });