Sfoglia il codice sorgente

Merge branch 'support/share-link-for-outside-for-merge' into improve/manage-share_link_for_outside

熊谷洸介(Kousuke Kumagai) 5 anni fa
parent
commit
66f61acfbc
33 ha cambiato i file con 578 aggiunte e 153 eliminazioni
  1. 7 0
      CHANGES.md
  2. 2 2
      resource/locales/en-US/translation.json
  3. 2 2
      resource/locales/ja/translation.json
  4. 0 2
      src/client/js/app.jsx
  5. 1 1
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  6. 2 2
      src/client/js/components/LoginForm.jsx
  7. 18 3
      src/client/js/components/Navbar/PersonalDropdown.jsx
  8. 17 6
      src/client/js/components/OutsideShareLinkModal.jsx
  9. 35 16
      src/client/js/components/Page/CopyDropdown.jsx
  10. 0 1
      src/client/js/components/PageDuplicateModal.jsx
  11. 0 1
      src/client/js/components/PagePathAutoComplete.jsx
  12. 64 0
      src/client/js/components/ShareLinkForm.jsx
  13. 49 0
      src/client/js/components/ShareLinkList.jsx
  14. 5 4
      src/client/js/services/AppContainer.js
  15. 10 55
      src/client/styles/scss/_login.scss
  16. 10 5
      src/client/styles/scss/_navbar.scss
  17. 3 3
      src/client/styles/scss/_override-bootstrap.scss
  18. 5 5
      src/client/styles/scss/_search.scss
  19. 12 0
      src/client/styles/scss/_sharelink.scss
  20. 1 1
      src/client/styles/scss/atoms/_buttons.scss
  21. 31 0
      src/client/styles/scss/atoms/_custom_control.scss
  22. 2 0
      src/client/styles/scss/molecules/copy-dropdown.scss
  23. 1 0
      src/client/styles/scss/style-app.scss
  24. 81 0
      src/client/styles/scss/theme/_apply-colors-dark.scss
  25. 63 0
      src/client/styles/scss/theme/_apply-colors-light.scss
  26. 4 0
      src/client/styles/scss/theme/_apply-colors.scss
  27. 10 3
      src/client/styles/scss/theme/antarctic.scss
  28. 15 11
      src/client/styles/scss/theme/christmas.scss
  29. 14 5
      src/client/styles/scss/theme/spring.scss
  30. 13 7
      src/client/styles/scss/theme/wood.scss
  31. 1 1
      src/server/models/share-link.js
  32. 93 12
      src/server/routes/apiv3/share-links.js
  33. 7 5
      src/server/views/layout/layout.html

+ 7 - 0
CHANGES.md

@@ -2,7 +2,14 @@
 
 ## v4.0.3-RC
 
+* Feature: Copy page path dropdown with Append params switch
+* Improvement: Truncate overflowed user browsing history
+* Improvement: Tabs appearance on mobile
+* Improvement: Search help appearance on mobile
+* Improvement: Accessibility of login page
+* Fix: Editor was broken by long lines
 * Fix: Editor doesn't work on mobile
+* Fix: Word break in Recent Updated contents
 * Fix: navbar is broken on Safari
 
 ## v4.0.2

+ 2 - 2
resource/locales/en-US/translation.json

@@ -61,8 +61,8 @@
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "User ID": "User ID",
-  "User's Home": "User's Home",
-  "User Settings": "User Settings",
+  "Home": "Home",
+  "Settings": "Settings",
   "User Information": "User information",
   "Basic Info": "Basic info",
   "Name": "Name",

+ 2 - 2
resource/locales/ja/translation.json

@@ -61,8 +61,8 @@
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "User ID": "ユーザーID",
-  "User's Home": "ユーザーホーム",
-  "User Settings": "ユーザー設定",
+  "Home": "ホーム",
+  "Settings": "設定",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",

+ 0 - 2
src/client/js/app.jsx

@@ -26,7 +26,6 @@ import PageShareManagement from './components/Page/PageShareManagement';
 import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
-import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
@@ -97,7 +96,6 @@ if (pageContainer.state.pageId != null) {
     'revision-toc': <TableOfContents />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
-    'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
 
     'user-created-list': <RecentCreated />,
     'user-draft-list': <MyDraftList />,

+ 1 - 1
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -59,7 +59,7 @@ class CustomizeScriptSetting extends React.Component {
               Placeholders:<br />
               (Available after <code>load</code> event)
             </div>
-            <table className="table table-borderless table-sm form-text text-muted offset-1">
+            <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
               <tbody>
                 <tr>
                   <th className="text-right"><code>$</code></th>

+ 2 - 2
src/client/js/components/LoginForm.jsx

@@ -116,7 +116,7 @@ class LoginForm extends React.Component {
 
     return (
       <>
-        <div className="border-top border-bottom">
+        <div className="grw-external-auth-form border-top border-bottom">
           <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
             <div className="row mt-2">
               {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
@@ -131,7 +131,7 @@ class LoginForm extends React.Component {
         <div className="text-center">
           <button
             type="button"
-            className="btn btn-secondary btn-sm rounded-0 mb-3"
+            className="btn btn-secondary btn-external-auth-tab btn-sm rounded-0 mb-3"
             data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
             data-target="#external-auth"
             aria-expanded="false"

+ 18 - 3
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -65,8 +65,23 @@ const PersonalDropdown = (props) => {
       {/* Menu */}
       <div className="dropdown-menu dropdown-menu-right">
 
-        <a className="dropdown-item" href={`/user/${user.username}`}><i className="icon-fw icon-user"></i>{ t('User\'s Home') }</a>
-        <a className="dropdown-item" href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a>
+        <div className="px-4 pt-3 pb-2 text-center">
+          <UserPicture user={user} size="lg" noLink noTooltip />
+
+          <h5 className="mt-2">
+            {user.name}
+          </h5>
+
+          <div className="my-2">
+            <i className="icon-user icon-fw"></i>{user.username}<br />
+            <i className="icon-envelope icon-fw"></i><span className="grw-email-sm">{user.email}</span>
+          </div>
+
+          <div className="btn-group btn-block mt-2" role="group">
+            <a className="btn btn-sm btn-outline-secondary" href={`/user/${user.username}`}><i className="icon-fw icon-home"></i>{ t('Home') }</a>
+            <a className="btn btn-sm btn-outline-secondary" href="/me"><i className="icon-fw icon-wrench"></i>{ t('Settings') }</a>
+          </div>
+        </div>
 
         <div className="dropdown-divider"></div>
 
@@ -107,7 +122,7 @@ const PersonalDropdown = (props) => {
 
         <div className="dropdown-divider"></div>
 
-        <a className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a>
+        <button type="button" className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</button>
       </div>
 
     </>

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

@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
+  Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
 import { withTranslation } from 'react-i18next';
@@ -12,20 +12,31 @@ 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; */
 
-
   return (
     <Modal size="lg" isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">Hi there!
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">Title
       </ModalHeader>
       <ModalBody>
-        <h1>Hi there</h1>
+        <div className="container">
+          <div className="row align-items-center mb-3">
+            <h4 className="col-10">Shared Link List</h4>
+            <button className="col btn btn-danger" type="button">Delete all links</button>
+          </div>
+
+          <div>
+            <ShareLinkList />
+            <button className="btn btn-outline-secondary d-block mx-auto px-5 mb-3" type="button">+</button>
+            <ShareLinkForm />
+          </div>
+        </div>
       </ModalBody>
-      <ModalFooter>
-      </ModalFooter>
     </Modal>
   );
 };

+ 35 - 16
src/client/js/components/Page/CopyDropdown.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import {
-  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Tooltip,
 } from 'reactstrap';
 
@@ -18,6 +18,7 @@ class CopyDropdown extends React.Component {
     this.state = {
       dropdownOpen: false,
       tooltipOpen: false,
+      isParamsAppended: true,
     };
 
     this.toggle = this.toggle.bind(this);
@@ -39,13 +40,22 @@ class CopyDropdown extends React.Component {
     }, 1000);
   }
 
-  generatePagePathWithParams() {
-    const { pagePath } = this.props;
+  get uriParams() {
+    const { isParamsAppended } = this.state;
+
+    if (!isParamsAppended) {
+      return '';
+    }
+
     const {
       search, hash,
     } = window.location;
+    return `${search}${hash}`;
+  }
 
-    return `${pagePath}${search}${hash}`;
+  generatePagePathWithParams() {
+    const { pagePath } = this.props;
+    return decodeURI(`${pagePath}${this.uriParams}`);
   }
 
   generatePagePathUrl() {
@@ -55,25 +65,18 @@ class CopyDropdown extends React.Component {
 
   generatePermalink() {
     const { pageId } = this.props;
-    const { location } = window;
 
     if (pageId == null) {
       return null;
     }
 
-    const {
-      origin, search, hash,
-    } = location;
-    return `${origin}/${pageId}${search}${hash}`;
+    return decodeURI(`${origin}/${pageId}${this.uriParams}`);
   }
 
   generateMarkdownLink() {
     const { pagePath } = this.props;
-    const {
-      search, hash,
-    } = window.location;
 
-    const label = `${pagePath}${search}${hash}`;
+    const label = decodeURI(`${pagePath}${this.uriParams}`);
     const permalink = this.generatePermalink();
 
     return `[${label}](${permalink})`;
@@ -88,6 +91,7 @@ class CopyDropdown extends React.Component {
 
   render() {
     const { t, pageId } = this.props;
+    const { isParamsAppended } = this.state;
 
     const pagePathWithParams = this.generatePagePathWithParams();
     const pagePathUrl = this.generatePagePathUrl();
@@ -97,7 +101,7 @@ class CopyDropdown extends React.Component {
 
     return (
       <>
-        <Dropdown id="copyPagePathDropdown" className="grw-copy-dropdown" isOpen={this.state.dropdownOpen} toggle={this.toggle}>
+        <UncontrolledDropdown id="copyPagePathDropdown" className="grw-copy-dropdown">
 
           <DropdownToggle
             caret
@@ -108,7 +112,22 @@ class CopyDropdown extends React.Component {
           </DropdownToggle>
 
           <DropdownMenu>
-            <DropdownItem header className="px-3">{ t('copy_to_clipboard.Copy to clipboard') }</DropdownItem>
+
+            <div className="d-flex align-items-center justify-content-between">
+              <DropdownItem header className="px-3">
+                { t('copy_to_clipboard.Copy to clipboard') }
+              </DropdownItem>
+              <div className="px-3 custom-control custom-switch custom-switch-sm">
+                <input
+                  type="checkbox"
+                  id="customSwitchForParams"
+                  className="custom-control-input"
+                  checked={isParamsAppended}
+                  onChange={e => this.setState({ isParamsAppended: !isParamsAppended })}
+                />
+                <label className="custom-control-label small" htmlFor="customSwitchForParams">Append params</label>
+              </div>
+            </div>
 
             <DropdownItem divider className="my-0"></DropdownItem>
 
@@ -162,7 +181,7 @@ class CopyDropdown extends React.Component {
             )}
           </DropdownMenu>
 
-        </Dropdown>
+        </UncontrolledDropdown>
 
         <Tooltip placement="bottom" isOpen={this.state.tooltipOpen} target="copyPagePathDropdown" fade={false}>
           copied!

+ 0 - 1
src/client/js/components/PageDuplicateModal.jsx

@@ -82,7 +82,6 @@ const PageDuplicateModal = (props) => {
                 <PagePathAutoComplete
                   crowi={appContainer}
                   initializedPath={path}
-                  addTrailingSlash
                   onSubmit={ppacSubmitHandler}
                   onInputChange={ppacInputChangeHandler}
                 />

+ 0 - 1
src/client/js/components/PagePathAutoComplete.jsx

@@ -61,7 +61,6 @@ PagePathAutoComplete.propTypes = {
 
 PagePathAutoComplete.defaultProps = {
   initializedPath: '/',
-  addTrailingSlash: true,
 };
 
 export default PagePathAutoComplete;

+ 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);

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

@@ -0,0 +1,49 @@
+import React from 'react';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+
+const ShareLinkList = (props) => {
+
+  function getShareLinkList() {
+    return ['Replace with API'];
+  }
+
+  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().map((shareLink) => {
+              return (
+                <>
+                  <td>{ shareLink }</td>
+                  <td>{ shareLink }</td>
+                  <td>{ shareLink }</td>
+                  <td>{ shareLink }</td>
+                </>
+              );
+            })
+          }
+        </tbody>
+      </table>
+    </div>
+  );
+};
+
+const ShareLinkListWrapper = (props) => {
+  return createSubscribedElement(ShareLinkList, props, [AppContainer]);
+};
+
+export default withTranslation()(ShareLinkListWrapper);

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

@@ -119,9 +119,9 @@ export default class AppContainer extends Container {
   }
 
   async initColorScheme() {
-    const switchStateByMediaQuery = (mql) => {
+    const switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
-      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
+      await this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
 
       this.applyColorScheme();
     };
@@ -130,13 +130,13 @@ export default class AppContainer extends Container {
     // add event listener
     mqlForDarkMode.addListener(switchStateByMediaQuery);
 
-    // restore settings from localStorage
+    // initialize1: restore settings from localStorage
     const { localStorage } = window;
     if (localStorage.preferDarkModeByUser != null) {
       await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
     }
 
-    // initialize
+    // initialize2: check media query
     switchStateByMediaQuery(mqlForDarkMode);
   }
 
@@ -413,6 +413,7 @@ export default class AppContainer extends Container {
    */
   applyColorScheme() {
     const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
+
     let isDarkMode = preferDarkModeByMediaQuery;
     if (preferDarkModeByUser != null) {
       isDarkMode = preferDarkModeByUser;

+ 10 - 55
src/client/styles/scss/_login.scss

@@ -1,12 +1,4 @@
 .nologin {
-  $gray-800-for-login: darken(white, 30%);
-  $color-gradient: #3e4d6c;
-
-  // background color
-  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-    linear-gradient(135deg, $growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, $growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
-    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
   #page-wrapper {
     background: none;
   }
@@ -62,43 +54,20 @@
 
   // styles
   .login-header {
-    background-color: rgba(white, 0.5);
-
-    .logo {
-      background-color: rgba(black, 0);
-      fill: rgba(black, 0.5);
-    }
-
     h1 {
       font-size: 22px;
       line-height: 1em;
-      color: rgba(black, 0.5);
     }
   }
 
-  .login-dialog {
-    background-color: rgba(white, 0.5);
-  }
-
   .input-group {
     margin-bottom: 10px;
 
     .input-group-text {
-      color: $gray-800-for-login;
       text-align: center;
-      background-color: rgba(black, 0.4);
       border: none;
       border-radius: 0;
     }
-
-    .form-control {
-      color: white;
-      background-color: rgba(lighten(black, 10%), 0.4);
-
-      &::placeholder {
-        color: $gray-800-for-login;
-      }
-    }
   }
 
   .input-group:not(.has-error) {
@@ -114,39 +83,39 @@
   $btn-fill-colors: (
     'login': (
       rgba($danger, 0.4),
-      rgba(#7e4153, 0.5),
+      rgba(#7e4153, 0.7),
     ),
     'register': (
       rgba($success, 0.4),
-      rgba(#3f7263, 0.5),
+      rgba(#3f7263, 0.7),
     ),
     'google': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
     'github': (
       rgba(lighten(black, 20%), 0.4),
-      #555,
+      #444,
     ),
     'facebook': (
       rgba(#29487d, 0.4),
-      #555,
+      #444,
     ),
     'twitter': (
       rgba(#1da1f2, 0.4),
-      #555,
+      #444,
     ),
     'oidc': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
     'saml': (
       rgba(#55a79a, 0.4),
-      #555,
+      #444,
     ),
     'basic': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
   );
 
@@ -165,26 +134,12 @@
   .link-growi-org {
     font-size: smaller;
     font-weight: bold;
-    color: rgba(black, 0.4);
 
     &,
     .growi,
     .org {
       transition: color 0.8s;
     }
-
-    &:hover,
-    &.focus {
-      color: black;
-
-      .growi {
-        color: darken($growi-green, 20%);
-      }
-
-      .org {
-        color: darken($growi-blue, 15%);
-      }
-    }
   }
 
   .link-switch {
@@ -210,7 +165,7 @@
   .link-growi-org {
     position: absolute;
     bottom: 9px;
-    z-index: 2;
+    z-index: 3;
   }
 
   // To adjust the behavior, this problem is not solved.

+ 10 - 5
src/client/styles/scss/_navbar.scss

@@ -4,6 +4,10 @@
   border-bottom: $grw-navbar-border-width solid;
   border-left: 0;
 
+  .grw-app-title {
+    @include variable-font-size(24px);
+  }
+
   .grw-navbar-toggler {
     padding: 0.5rem;
     font-size: 1.5em;
@@ -23,11 +27,6 @@
     padding: 0 1rem;
   }
 
-  #personal-dropdown::after {
-    // hide caret
-    content: none;
-  }
-
   .nav-link {
     &:hover {
       background: rgba(0, 0, 0, 0.1);
@@ -40,4 +39,10 @@
   .nav-item.confidential {
     background: rgba(0, 0, 0, 0.2);
   }
+
+  .grw-personal-dropdown {
+    .grw-email-sm {
+      font-size: 0.75em;
+    }
+  }
 }

+ 3 - 3
src/client/styles/scss/_override-bootstrap.scss

@@ -77,13 +77,13 @@
   }
 
   // Dropdowns
-  .dropdown {
-    .dropdown-toggle.btn.disabled {
+  .dropdown-toggle {
+    &.btn.disabled {
       cursor: not-allowed;
     }
 
     // hide caret
-    .dropdown-toggle.dropdown-toggle-no-caret::after {
+    &.dropdown-toggle-no-caret::after {
       content: none;
     }
   }

+ 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 - 1
src/client/styles/scss/atoms/_buttons.scss

@@ -41,7 +41,7 @@
   color: white;
   text-align: center;
   cursor: pointer;
-  background-color: rgba(lighten(black, 20%), 0.4);
+  background-color: rgba(lighten(black, 15%), 0.5);
   border: none;
 
   .btn-label {

+ 31 - 0
src/client/styles/scss/atoms/_custom_control.scss

@@ -1,3 +1,34 @@
 label.custom-control-label {
   font-weight: normal;
 }
+
+.custom-switch.custom-switch-sm {
+  $custom-control-indicator-size-sm: $custom-control-indicator-size * 0.8;
+  $custom-switch-width-sm: $custom-control-indicator-size-sm * 1.75;
+  $custom-control-gutter-sm: $custom-control-gutter * 0.8;
+  $custom-control-indicator-size-sm: $custom-control-indicator-size * 0.8;
+  $custom-switch-indicator-size-sm: subtract($custom-control-indicator-size-sm, $custom-control-indicator-border-width * 4);
+
+  padding-left: $custom-switch-width-sm + $custom-control-gutter-sm;
+
+  .custom-control-label {
+    &::before {
+      left: -($custom-switch-width-sm + $custom-control-gutter-sm);
+      width: $custom-switch-width-sm;
+      height: $custom-control-indicator-size-sm;
+    }
+
+    &::after {
+      top: add(($font-size-base * $line-height-base - $custom-control-indicator-size) / 2, $custom-control-indicator-border-width * 2);
+      left: add(-($custom-switch-width-sm + $custom-control-gutter-sm), $custom-control-indicator-border-width * 2);
+      width: $custom-switch-indicator-size-sm;
+      height: $custom-switch-indicator-size-sm;
+    }
+  }
+
+  .custom-control-input:checked ~ .custom-control-label {
+    &::after {
+      transform: translateX($custom-switch-width-sm - $custom-control-indicator-size-sm);
+    }
+  }
+}

+ 2 - 0
src/client/styles/scss/molecules/copy-dropdown.scss

@@ -1,5 +1,7 @@
 .grw-copy-dropdown {
   .dropdown-menu {
+    min-width: 310px;
+
     .dropdown-header {
       margin-bottom: 0.5em;
       font-size: 1.1em;

+ 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

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

@@ -94,6 +94,87 @@ ul.pagination {
   }
 }
 
+/*
+ * GROWI Login form
+ */
+.nologin {
+  // background color
+  $color-gradient: #3c465c;
+  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
+    linear-gradient(135deg, darken($growi-green, 30%) 10%, hsla(225, 95%, 50%, 0) 70%),
+    linear-gradient(225deg, darken($growi-blue, 20%) 10%, hsla(140, 90%, 50%, 0) 80%),
+    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
+
+  .login-header {
+    background-color: rgba(black, 0.5);
+
+    .logo {
+      background-color: rgba(white, 0);
+      fill: rgba(white, 0.5);
+    }
+
+    h1 {
+      color: rgba(white, 0.5);
+    }
+  }
+
+  .login-dialog {
+    background-color: rgba(black, 0.5);
+  }
+
+  .input-group {
+    .input-group-text {
+      color: darken(white, 30%);
+      background-color: rgba(#444, 0.7);
+    }
+
+    .form-control {
+      color: white;
+      background-color: rgba(#505050, 0.7);
+      box-shadow: unset;
+
+      &::placeholder {
+        color: darken(white, 30%);
+      }
+    }
+  }
+
+  .btn-fill {
+    .btn-label {
+      color: #ccc;
+    }
+    .btn-label-text {
+      color: #aaa;
+    }
+  }
+
+  .grw-external-auth-form {
+    border-color: gray !important;
+  }
+
+  .btn-external-auth-tab {
+    @extend .btn-dark;
+  }
+
+  // footer link text
+  .link-growi-org {
+    color: rgba(white, 0.4);
+
+    &:hover,
+    &.focus {
+      color: rgba(white, 0.7);
+
+      .growi {
+        color: darken($growi-green, 5%);
+      }
+
+      .org {
+        color: darken($growi-blue, 5%);
+      }
+    }
+  }
+}
+
 /*
  * GROWI page list
  */

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

@@ -38,6 +38,69 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: darken($bgcolor-global, 5%);
 }
 
+/*
+ * GROWI Login form
+ */
+.nologin {
+  // background color
+  $color-gradient: #3e4d6c;
+  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
+    linear-gradient(135deg, $growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, $growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
+    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
+
+  .login-header {
+    background-color: rgba(white, 0.5);
+
+    .logo {
+      background-color: rgba(black, 0);
+      fill: rgba(black, 0.5);
+    }
+
+    h1 {
+      color: rgba(black, 0.5);
+    }
+  }
+
+  .login-dialog {
+    background-color: rgba(white, 0.5);
+  }
+
+  .input-group {
+    .input-group-text {
+      color: darken(white, 30%);
+      background-color: rgba(#444, 0.7);
+    }
+
+    .form-control {
+      color: white;
+      background-color: rgba(#505050, 0.7);
+      box-shadow: unset;
+
+      &::placeholder {
+        color: darken(white, 30%);
+      }
+    }
+  }
+
+  // footer link text
+  .link-growi-org {
+    color: rgba(black, 0.4);
+
+    &:hover,
+    &.focus {
+      color: black;
+
+      .growi {
+        color: darken($growi-green, 20%);
+      }
+
+      .org {
+        color: darken($growi-blue, 15%);
+      }
+    }
+  }
+}
+
 /*
  * GROWI subnavigation
  */

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

@@ -148,6 +148,10 @@ pre:not(.hljs):not(.CodeMirror-line) {
 
   border-image: $border-image-navbar;
   border-image-slice: 1;
+
+  .grw-app-title {
+    color: $fillcolor-logo-mark;
+  }
 }
 
 .search-top {

+ 10 - 3
src/client/styles/scss/theme/antarctic.scss

@@ -123,9 +123,16 @@ html[dark] {
     }
   }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: rgba(black, 0.5);
+  // login and register
+  .nologin {
+    a#login.link-switch,
+    a#register.link-switch {
+      color: rgba(black, 0.5);
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
+    }
   }
 }
 

+ 15 - 11
src/client/styles/scss/theme/christmas.scss

@@ -135,22 +135,26 @@ html[dark] {
   // login page
   .nologin {
     .input-group {
-      .input-group-addon {
-        background-color: rgba(lighten(black, 10%), 0.6);
+      .input-group-text {
+        color: #444;
+        background-color: rgba(darken(white, 20%), 0.6);
       }
       .form-control {
-        background-color: rgba(lighten(black, 10%), 0.6);
+        color: #444;
+        background-color: rgba(white, 0.6);
       }
     }
 
-    &.login-page {
-      .login-header,
-      .login-dialog {
-        background-color: rgba(#ccc, 0.5);
-      }
-      .link-switch {
-        color: #bd3425;
-      }
+    .login-header,
+    .login-dialog {
+      background-color: rgba(#ccc, 0.5);
+    }
+    .link-switch {
+      color: #bd3425;
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
     }
   }
 

+ 14 - 5
src/client/styles/scss/theme/spring.scss

@@ -104,7 +104,8 @@ html[dark] {
     }
   }
 
-  .growi.login-page {
+  // login and register
+  .nologin {
     #page-wrapper {
       background-color: $themelight;
       background-image: url('/images/themes/spring/spring.svg');
@@ -112,11 +113,19 @@ html[dark] {
       background-position: bottom;
       background-size: cover;
     }
-  }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: $color-global;
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
+
+    .link-switch {
+      color: $color-global;
+    }
+
+    .grw-external-auth-form {
+      border-color: $accentcolor !important;
+    }
   }
 
   .table {

+ 13 - 7
src/client/styles/scss/theme/wood.scss

@@ -144,14 +144,20 @@ html[dark] {
   }
 
   // login and register
+  .nologin {
+    background: white;
 
-  .login-header,
-  .login-dialog {
-    background-color: rgba(black, 0.1);
-  }
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: rgba(black, 0.5);
+    .link-switch {
+      color: rgba(black, 0.5);
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
+    }
   }
 }

+ 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
   });
 

+ 7 - 5
src/server/views/layout/layout.html

@@ -75,16 +75,18 @@
 
   {% block layout_head_nav %}
   <nav class="navbar grw-navbar navbar-expand navbar-dark fixed-top mb-0 px-0">
-    <ul class="navbar-nav d-md-none mr-auto">
-      <li id="grw-navbar-toggler" class="nav-item"></li>
-    </ul>
-
     {# Brand Logo #}
     <div class="navbar-brand mr-0">
       <a class="grw-logo d-block" href="/">
         {% include '../widget/logo.html' %}
       </a>
     </div>
+    <ul class="navbar-nav d-md-none">
+      <li id="grw-navbar-toggler" class="nav-item"></li>
+    </ul>
+    <div class="grw-app-title d-none d-md-block">
+      {{ appService.getAppTitle() | preventXss }}
+    </div>
 
     {# Navbar Right #}
     <ul class="navbar-nav ml-auto">
@@ -97,7 +99,7 @@
             </a>
           </li>
         {% endif %}
-        <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
+        <li id="personal-dropdown" class="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret"></li>
       {% else %}
         <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
       {% endif %}