Browse Source

Merge branch 'imprv/duplicate-Page-with-child' into apiv3-for-duplicate

zahmis 5 years ago
parent
commit
a26315e2dc
60 changed files with 584 additions and 367 deletions
  1. 2 2
      CHANGES.md
  2. BIN
      public/images/agile-admin/tooltip/Euclid.png
  3. 0 8
      public/images/agile-admin/tooltip/shape1.svg
  4. 0 18
      public/images/agile-admin/tooltip/shape2.svg
  5. 0 5
      public/images/agile-admin/tooltip/shape3.svg
  6. 0 8
      public/images/agile-admin/tooltip/tooltip1.svg
  7. 0 6
      public/images/agile-admin/tooltip/tooltip2.svg
  8. 0 6
      public/images/agile-admin/tooltip/tooltip3.svg
  9. 29 67
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  10. 62 0
      src/client/js/components/Admin/App/AppSettingsPageContents.jsx
  11. 1 1
      src/client/js/components/BookmarkButton.jsx
  12. 7 8
      src/client/js/components/EmptyTrashModal.jsx
  13. 1 1
      src/client/js/components/LikeButton.jsx
  14. 6 8
      src/client/js/components/PageDeleteModal.jsx
  15. 11 11
      src/client/js/components/PageDuplicateModal.jsx
  16. 23 0
      src/client/js/components/PageManagement/ApiErrorMessageList.jsx
  17. 8 10
      src/client/js/components/PageRenameModal.jsx
  18. 5 8
      src/client/js/components/PutbackPageModal.jsx
  19. 3 1
      src/client/js/services/AdminAppContainer.js
  20. 9 11
      src/client/js/services/PageContainer.js
  21. 3 3
      src/client/styles/scss/_admin.scss
  22. 1 1
      src/client/styles/scss/_comment.scss
  23. 2 2
      src/client/styles/scss/_comment_kibela.scss
  24. 4 4
      src/client/styles/scss/_editor-attachment.scss
  25. 1 1
      src/client/styles/scss/_editor-overlay.scss
  26. 4 4
      src/client/styles/scss/_hljs.scss
  27. 5 5
      src/client/styles/scss/_layout.scss
  28. 7 7
      src/client/styles/scss/_login.scss
  29. 1 1
      src/client/styles/scss/_navbar_kibela.scss
  30. 7 6
      src/client/styles/scss/_on-edit.scss
  31. 1 1
      src/client/styles/scss/_override-bootstrap-variables.scss
  32. 4 4
      src/client/styles/scss/_page.scss
  33. 3 3
      src/client/styles/scss/_page_list.scss
  34. 5 5
      src/client/styles/scss/_search.scss
  35. 3 3
      src/client/styles/scss/_shortcuts.scss
  36. 1 1
      src/client/styles/scss/_subnav.scss
  37. 2 2
      src/client/styles/scss/_user.scss
  38. 1 0
      src/client/styles/scss/_wiki.scss
  39. 16 9
      src/client/styles/scss/atoms/_buttons.scss
  40. 5 5
      src/client/styles/scss/style-presentation.scss
  41. 5 5
      src/client/styles/scss/theme/_apply-colors-dark.scss
  42. 2 2
      src/client/styles/scss/theme/_apply-colors-kibela.scss
  43. 7 7
      src/client/styles/scss/theme/antarctic.scss
  44. 9 9
      src/client/styles/scss/theme/christmas.scss
  45. 5 5
      src/client/styles/scss/theme/default.scss
  46. 1 1
      src/client/styles/scss/theme/future.scss
  47. 3 3
      src/client/styles/scss/theme/halloween.scss
  48. 5 5
      src/client/styles/scss/theme/island.scss
  49. 1 1
      src/client/styles/scss/theme/kibela.scss
  50. 4 4
      src/client/styles/scss/theme/mono-blue.scss
  51. 4 4
      src/client/styles/scss/theme/nature.scss
  52. 3 3
      src/client/styles/scss/theme/spring.scss
  53. 4 4
      src/client/styles/scss/theme/wood.scss
  54. 5 5
      src/linter-checker/test.scss
  55. 7 1
      src/server/middlewares/access-token-parser.js
  56. 191 11
      src/server/routes/apiv3/pages.js
  57. 0 1
      src/server/routes/index.js
  58. 1 49
      src/server/routes/page.js
  59. 1 1
      src/server/views/admin/customize.html
  60. 83 0
      src/test/middlewares/access-token-parser.test.js

+ 2 - 2
CHANGES.md

@@ -8,8 +8,8 @@
     * Page history
     * Page history
     * Renaming pages
     * Renaming pages
     * Deleting pages
     * Deleting pages
-* Fix: "Append params" switch of CopyDropdown does not work when multiple CopyDropdown instance exists\
-
+* Fix: "Append params" switch of CopyDropdown does not work when multiple CopyDropdown instance exists
+* Fix: Access token parser
 
 
 ## v4.1.0
 ## v4.1.0
 
 

BIN
public/images/agile-admin/tooltip/Euclid.png


+ 0 - 8
public/images/agile-admin/tooltip/shape1.svg

@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" viewBox="0 0 200 200" preserveAspectRatio="none">
-<path fill="#00AEEF" d="M174.209,28.162C154.645,8.88,124.289,2.08,100.06,2.08c-0.074,0-0.06-0.079-0.06-0.079
-	s0.015,0.079-0.06,0.079c-24.229,0-54.584,6.8-74.149,26.082C5.417,48.242,3,75,3,100s2.418,51.758,22.792,71.838
-	c19.564,19.281,49.92,26.082,74.149,26.082c0.074,0,0.06,0.079,0.06,0.079s-0.015-0.079,0.06-0.079
-	c24.229,0,54.585-6.801,74.149-26.082C194.582,151.758,197,125,197,100S194.582,48.242,174.209,28.162z"/>
-</svg>

+ 0 - 18
public/images/agile-admin/tooltip/shape2.svg

@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200px" height="150px" viewBox="0 0 200 150">
-<g>
-	<path id="path1" fill="#010101" d="M159.599,137.909c0.975,3.397,4.717,5.548,8.161,4.988c3.489-0.443,6.558-3.466,6.685-7.043
-		c0.217-3.19-1.805-6.34-5.113-7.118c-3.417-1.079-7.469,0.508-9.138,3.701c-0.91,1.636-1.166,3.624-0.612,5.414"/>
-	<path id="path2" fill="#010101" d="M130.646,125.253c1.368,4.656,6.393,7.288,10.806,6.718c4.763-0.451,9.26-4.276,9.71-9.394
-		c0.369-3.779-1.902-7.583-5.244-9.144c-5.404-2.732-12.557-0.222-14.908,5.448c-0.841,1.945-1.018,4.214-0.388,6.294"/>
-	<path id="path3" fill="#010101" d="M184.112,144.325c0.704,2.461,3.412,4.016,5.905,3.611c2.526-0.318,4.746-2.509,4.841-5.093
-		c0.153-2.315-1.483-4.54-3.703-5.155c-2.474-0.781-5.405,0.37-6.612,2.681c-0.657,1.181-0.845,2.619-0.442,3.917"/>
-	<path id="path4" fill="#010101" d="M53.149,10.686c12.101-3.695,24.478-1.625,33.84,4.571c3.187-5.687,8.381-10.144,14.943-12.148
-		c10.427-3.185,21.37,0.699,28.159,8.982c15.606-3.76,31.369,4.398,35.804,18.915c3.269,10.699-0.488,21.956-8.71,29.388
-		c0.395,0.934,0.762,1.882,1.064,2.873c4.73,15.485-3.992,31.889-19.473,36.617c-5.073,1.551-10.251,1.625-15.076,0.518
-		c-3.58,10.605-12.407,19.55-24.386,23.211c-15.015,4.586-30.547-0.521-39.226-11.624c-2.861,1.991-6.077,3.564-9.583,4.636
-		c-18.43,5.631-38.04-5.068-43.785-23.874l-0.083-0.272C1.564,75.375,9.696,57.543,25.083,50.302
-		C23.349,33.157,34.85,16.276,53.149,10.686L53.149,10.686z"/>
-</g>
-</svg>

+ 0 - 5
public/images/agile-admin/tooltip/shape3.svg

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200px" height="150px" viewBox="0 0 200 150" enable-background="new 0 0 200 150" xml:space="preserve">
-<polygon fill="#FFFFFF" stroke="#000000" points="29.857,3.324 171.111,3.324 196.75,37.671 184.334,107.653 104.355,136.679 100,146.676 96.292,136.355 16.312,107.653 3.25,37.671 "/>
-</svg>

+ 0 - 8
public/images/agile-admin/tooltip/tooltip1.svg

@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30px" height="20px" viewBox="0 0 30 20">
-	<g>
-		<path fill="#fb9678" d="M7.065,7.067C13.462,10.339,15,19.137,15,19.137V0H0C0,0,1.865,4.407,7.065,7.067z"/>
-		<path fill="#fb9678" d="M15,0v19.137c0,0,1.537-8.797,7.936-12.07C28.135,4.407,30,0,30,0H15z"/>
-	</g>
-</svg>

+ 0 - 6
public/images/agile-admin/tooltip/tooltip2.svg

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80px" height="80px" viewBox="0 0 80 80">
-<path fill="#e35583" d="M80,0c0,0-5.631,14.445-25.715,27.213C29.946,42.688,12.79,33.997,3.752,30.417
-	c-3.956-1.567-4.265,1.021-2.966,3.814C16.45,67.934,80,79.614,80,79.614l0,0V0z"/>
-</svg>

+ 0 - 6
public/images/agile-admin/tooltip/tooltip3.svg

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60px" height="120px" preserveAspectRatio="none" viewBox="0 0 60 120">
-<path fill="#ffffff" d="M55.451-0.043C55.451-0.043,66.059-41.066,55.451-0.043C51.069,16.9,0.332,119.498,0.332,119.498
-	S43.365,18.315,39.532-0.043c-4.099-19.616,0,0,0,0"/>
-</svg>

+ 29 - 67
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -1,92 +1,54 @@
-import React, { Fragment } from 'react';
-import { withTranslation } from 'react-i18next';
+import React, { Suspense } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 
 
-import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 
 
-import AppSetting from './AppSetting';
-import SiteUrlSetting from './SiteUrlSetting';
-import MailSetting from './MailSetting';
-import AwsSetting from './AwsSetting';
-import PluginSetting from './PluginSetting';
+import AppSettingsPageContents from './AppSettingsPageContents';
 
 
 const logger = loggerFactory('growi:appSettings');
 const logger = loggerFactory('growi:appSettings');
 
 
-class AppSettingsPage extends React.Component {
-
-  async componentDidMount() {
-    const { adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.retrieveAppSettingsData();
-    }
-    catch (err) {
-      toastError(err);
-      adminAppContainer.setState({ retrieveError: err.message });
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Fragment>
+function AppSettingsPage(props) {
+  return (
+    <Suspense
+      fallback={(
         <div className="row">
         <div className="row">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('App Settings')}</h2>
-            <AppSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
-            <SiteUrlSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:app_setting.mail_settings')}</h2>
-            <MailSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:app_setting.aws_settings')}</h2>
-            <AwsSetting />
-          </div>
+          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
         </div>
         </div>
+)}
+    >
+      <RenderAppSettingsPageWrapper />
+    </Suspense>
+  );
+}
 
 
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
-            <PluginSetting />
-          </div>
-        </div>
-      </Fragment>
-    );
+function RenderAppSettingsPage(props) {
+  if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
+    throw new Promise(async() => {
+      try {
+        await props.adminAppContainer.retrieveAppSettingsData();
+      }
+      catch (err) {
+        toastError(err);
+        props.adminAppContainer.setState({ retrieveError: err.message });
+        logger.error(err);
+      }
+    });
   }
   }
 
 
+  return <AppSettingsPageContents />;
 }
 }
 
 
-AppSettingsPage.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+RenderAppSettingsPage.propTypes = {
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const AppSettingsPageWrapper = withUnstatedContainers(AppSettingsPage, [AppContainer, AdminAppContainer]);
-
+const RenderAppSettingsPageWrapper = withUnstatedContainers(RenderAppSettingsPage, [AdminAppContainer]);
 
 
-export default withTranslation()(AppSettingsPageWrapper);
+export default AppSettingsPage;

+ 62 - 0
src/client/js/components/Admin/App/AppSettingsPageContents.jsx

@@ -0,0 +1,62 @@
+import React, { Fragment } from 'react';
+import { withTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+
+import AppSetting from './AppSetting';
+import SiteUrlSetting from './SiteUrlSetting';
+import MailSetting from './MailSetting';
+import AwsSetting from './AwsSetting';
+import PluginSetting from './PluginSetting';
+
+class AppSettingsPageContents extends React.Component {
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <div className="row">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('App Settings')}</h2>
+            <AppSetting />
+          </div>
+        </div>
+
+        <div className="row mt-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
+            <SiteUrlSetting />
+          </div>
+        </div>
+
+        <div className="row mt-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('admin:app_setting.mail_settings')}</h2>
+            <MailSetting />
+          </div>
+        </div>
+
+        <div className="row mt-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('admin:app_setting.aws_settings')}</h2>
+            <AwsSetting />
+          </div>
+        </div>
+
+        <div className="row mt-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
+            <PluginSetting />
+          </div>
+        </div>
+      </Fragment>
+    );
+  }
+
+}
+
+AppSettingsPageContents.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+};
+
+export default withTranslation()(AppSettingsPageContents);

+ 1 - 1
src/client/js/components/BookmarkButton.jsx

@@ -66,7 +66,7 @@ class BookmarkButton extends React.Component {
         onClick={this.handleClick}
         onClick={this.handleClick}
         className={`btn rounded-circle btn-bookmark border-0 d-edit-none
         className={`btn rounded-circle btn-bookmark border-0 d-edit-none
           ${`btn-${this.props.size}`}
           ${`btn-${this.props.size}`}
-          ${this.state.isBookmarked ? 'btn-warning active' : 'btn-outline-warning'}`}
+          ${this.state.isBookmarked ? 'active' : ''}`}
       >
       >
         <i className="icon-star"></i>
         <i className="icon-star"></i>
       </button>
       </button>

+ 7 - 8
src/client/js/components/EmptyTrashModal.jsx

@@ -9,25 +9,24 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const EmptyTrashModal = (props) => {
 const EmptyTrashModal = (props) => {
   const {
   const {
     t, isOpen, onClose, appContainer,
     t, isOpen, onClose, appContainer,
   } = props;
   } = props;
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
 
   async function emptyTrash() {
   async function emptyTrash() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
+
     try {
     try {
       await appContainer.apiv3Delete('/pages/empty-trash');
       await appContainer.apiv3Delete('/pages/empty-trash');
       window.location.reload();
       window.location.reload();
     }
     }
     catch (err) {
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
     }
   }
   }
 
 
@@ -44,7 +43,7 @@ const EmptyTrashModal = (props) => {
         { t('modal_empty.notice')}
         { t('modal_empty.notice')}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
         <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
           <i className="icon-trash mr-2" aria-hidden="true"></i> Empty
           <i className="icon-trash mr-2" aria-hidden="true"></i> Empty
         </button>
         </button>

+ 1 - 1
src/client/js/components/LikeButton.jsx

@@ -44,7 +44,7 @@ class LikeButton extends React.Component {
         type="button"
         type="button"
         onClick={this.handleClick}
         onClick={this.handleClick}
         className={`btn rounded-circle btn-like border-0 d-edit-none
         className={`btn rounded-circle btn-like border-0 d-edit-none
-        ${this.state.isLiked ? 'btn-info active' : 'btn-outline-info'}`}
+        ${this.state.isLiked ? 'active' : ''}`}
       >
       >
         <i className="icon-like"></i>
         <i className="icon-like"></i>
       </button>
       </button>

+ 6 - 8
src/client/js/components/PageDeleteModal.jsx

@@ -10,7 +10,7 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
 
 
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const deleteIconAndKey = {
 const deleteIconAndKey = {
   completely: {
   completely: {
@@ -32,8 +32,8 @@ const PageDeleteModal = (props) => {
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
   const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
   const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
 
   function changeIsDeleteRecursivelyHandler() {
   function changeIsDeleteRecursivelyHandler() {
     setIsDeleteRecursively(!isDeleteRecursively);
     setIsDeleteRecursively(!isDeleteRecursively);
@@ -47,8 +47,7 @@ const PageDeleteModal = (props) => {
   }
   }
 
 
   async function deletePage() {
   async function deletePage() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
 
 
     try {
     try {
       const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
       const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
@@ -56,8 +55,7 @@ const PageDeleteModal = (props) => {
       window.location.href = encodeURI(trashPagePath);
       window.location.href = encodeURI(trashPagePath);
     }
     }
     catch (err) {
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
     }
   }
   }
 
 
@@ -124,7 +122,7 @@ const PageDeleteModal = (props) => {
         {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
         {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className={`btn btn-${deleteIconAndKey[deleteMode].color}`} onClick={deleteButtonHandler}>
         <button type="button" className={`btn btn-${deleteIconAndKey[deleteMode].color}`} onClick={deleteButtonHandler}>
           <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }

+ 11 - 11
src/client/js/components/PageDuplicateModal.jsx

@@ -11,7 +11,7 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const PageDuplicateModal = (props) => {
 const PageDuplicateModal = (props) => {
   const { t, appContainer, pageContainer } = props;
   const { t, appContainer, pageContainer } = props;
@@ -22,8 +22,9 @@ const PageDuplicateModal = (props) => {
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
+
   const [subordinatedPaths, setSubordinatedPaths] = useState([]);
   const [subordinatedPaths, setSubordinatedPaths] = useState([]);
   const [getSubordinatedError, setGetSuborinatedError] = useState(null);
   const [getSubordinatedError, setGetSuborinatedError] = useState(null);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
@@ -65,16 +66,15 @@ const PageDuplicateModal = (props) => {
   }, [props.isOpen, getSubordinatedList]);
   }, [props.isOpen, getSubordinatedList]);
 
 
   async function duplicate() {
   async function duplicate() {
+    setErrs(null);
+
     try {
     try {
-      setErrorCode(null);
-      setErrorMessage(null);
-      const res = await appContainer.apiv3Post('/pages/duplicate', { pageId, pageNameInput });
-      const { result } = res;
-      window.location.href = encodeURI(`${result}?duplicated=${path}`);
+      const res = await appContainer.apiPost('/pages/duplicate', { page_id: pageId, new_path: pageNameInput });
+      const { page } = res;
+      window.location.href = encodeURI(`${page.path}?duplicated=${path}`);
     }
     }
     catch (err) {
     catch (err) {
-      setErrorCode(err[0].code);
-      setErrorMessage(err[0].message);
+      setErrs(err);
     }
     }
   }
   }
 
 
@@ -139,7 +139,7 @@ const PageDuplicateModal = (props) => {
         </div>
         </div>
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} targetPath={pageNameInput} />
+        <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button type="button" className="btn btn-primary" onClick={duplicate}>Duplicate page</button>
         <button type="button" className="btn btn-primary" onClick={duplicate}>Duplicate page</button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>

+ 23 - 0
src/client/js/components/PageManagement/ApiErrorMessageList.jsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ApiErrorMessage from './ApiErrorMessage';
+import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
+
+function ApiErrorMessageList(props) {
+  const errs = toArrayIfNot(props.errs);
+
+  return (
+    <>
+      {errs.map(err => <ApiErrorMessage key={err.code} errorCode={err.code} errorMessage={err.message} targetPath={props.targetPath} />)}
+    </>
+  );
+
+}
+
+ApiErrorMessageList.propTypes = {
+  errs:         PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+  targetPath:   PropTypes.string,
+};
+
+export default ApiErrorMessageList;

+ 8 - 10
src/client/js/components/PageRenameModal.jsx

@@ -11,7 +11,7 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const PageRenameModal = (props) => {
 const PageRenameModal = (props) => {
   const {
   const {
@@ -23,8 +23,8 @@ const PageRenameModal = (props) => {
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
 
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
@@ -51,10 +51,9 @@ const PageRenameModal = (props) => {
   }
   }
 
 
   async function rename() {
   async function rename() {
-    try {
-      setErrorCode(null);
-      setErrorMessage(null);
+    setErrs(null);
 
 
+    try {
       const response = await pageContainer.rename(
       const response = await pageContainer.rename(
         pageNameInput,
         pageNameInput,
         isRenameRecursively,
         isRenameRecursively,
@@ -62,7 +61,7 @@ const PageRenameModal = (props) => {
         isRenameMetadata,
         isRenameMetadata,
       );
       );
 
 
-      const { page } = response;
+      const { page } = response.data;
       const url = new URL(page.path, 'https://dummy');
       const url = new URL(page.path, 'https://dummy');
       url.searchParams.append('renamedFrom', path);
       url.searchParams.append('renamedFrom', path);
       if (isRenameRedirect) {
       if (isRenameRedirect) {
@@ -72,8 +71,7 @@ const PageRenameModal = (props) => {
       window.location.href = `${url.pathname}${url.search}`;
       window.location.href = `${url.pathname}${url.search}`;
     }
     }
     catch (err) {
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
     }
   }
   }
 
 
@@ -150,7 +148,7 @@ const PageRenameModal = (props) => {
         </div>
         </div>
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} targetPath={pageNameInput} />
+        <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button type="button" className="btn btn-primary" onClick={rename}>Rename</button>
         <button type="button" className="btn btn-primary" onClick={rename}>Rename</button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>

+ 5 - 8
src/client/js/components/PutbackPageModal.jsx

@@ -11,15 +11,14 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
 
 
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const PutBackPageModal = (props) => {
 const PutBackPageModal = (props) => {
   const {
   const {
     t, isOpen, onClose, pageContainer, path,
     t, isOpen, onClose, pageContainer, path,
   } = props;
   } = props;
 
 
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+  const [errs, setErrs] = useState(null);
 
 
   const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
   const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
 
 
@@ -28,8 +27,7 @@ const PutBackPageModal = (props) => {
   }
   }
 
 
   async function putbackPage() {
   async function putbackPage() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
 
 
     try {
     try {
       const response = await pageContainer.revertRemove(isPutbackRecursively);
       const response = await pageContainer.revertRemove(isPutbackRecursively);
@@ -37,8 +35,7 @@ const PutBackPageModal = (props) => {
       window.location.href = encodeURI(putbackPagePath);
       window.location.href = encodeURI(putbackPagePath);
     }
     }
     catch (err) {
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
     }
   }
   }
 
 
@@ -73,7 +70,7 @@ const PutBackPageModal = (props) => {
         </div>
         </div>
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
         <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
         </button>
         </button>

+ 3 - 1
src/client/js/services/AdminAppContainer.js

@@ -16,10 +16,12 @@ export default class AdminAppContainer extends Container {
     super();
     super();
 
 
     this.appContainer = appContainer;
     this.appContainer = appContainer;
+    this.dummyTitle = 0;
 
 
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
-      title: '',
+      // set dummy value tile for using suspense
+      title: this.dummyTitle,
       confidential: '',
       confidential: '',
       globalLang: '',
       globalLang: '',
       fileUpload: '',
       fileUpload: '',

+ 9 - 11
src/client/js/services/PageContainer.js

@@ -379,19 +379,17 @@ export default class PageContainer extends Container {
     });
     });
   }
   }
 
 
-  rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
+  rename(newPagePath, isRecursively, isRenameRedirect, isRemainMetadata) {
     const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
     const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const isRecursively = isRenameRecursively ? true : null;
-    const isRedirect = isRenameRedirect ? true : null;
-    const isRemain = isRenameMetadata ? true : null;
+    const { pageId, revisionId } = this.state;
 
 
-    return this.appContainer.apiPost('/pages.rename', {
-      recursively: isRecursively,
-      page_id: this.state.pageId,
-      revision_id: this.state.revisionId,
-      new_path: pageNameInput,
-      create_redirect: isRedirect,
-      remain_metadata: isRemain,
+    return this.appContainer.apiv3Put('/pages/rename', {
+      revisionId,
+      pageId,
+      isRecursively,
+      isRenameRedirect,
+      isRemainMetadata,
+      newPagePath,
       socketClientId: socketIoContainer.getSocketClientId(),
       socketClientId: socketIoContainer.getSocketClientId(),
     });
     });
   }
   }

+ 3 - 3
src/client/styles/scss/_admin.scss

@@ -28,7 +28,7 @@
 
 
     .ss-container img {
     .ss-container img {
       padding: 0.5em;
       padding: 0.5em;
-      background-color: #ddd;
+      background-color: $gray-300;
     }
     }
 
 
     .table-user-list {
     .table-user-list {
@@ -140,8 +140,8 @@
 
 
     // style
     // style
     .theme-option-container a {
     .theme-option-container a {
-      background-color: #f5f5f5;
-      border: 1px solid #ccc;
+      background-color: $gray-50;
+      border: 1px solid $gray-300;
     }
     }
     .theme-option-name {
     .theme-option-name {
       opacity: 0.3;
       opacity: 0.3;

+ 1 - 1
src/client/styles/scss/_comment.scss

@@ -38,7 +38,7 @@
       justify-content: flex-end;
       justify-content: flex-end;
 
 
       font-size: 0.9em;
       font-size: 0.9em;
-      color: #999;
+      color: $gray-400;
     }
     }
   }
   }
 
 

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

@@ -14,7 +14,7 @@
       height: 0;
       height: 0;
       content: '';
       content: '';
       border-top: 20px solid transparent;
       border-top: 20px solid transparent;
-      border-right: 20px solid #e6e9ec;
+      border-right: 20px solid $gray-200;
       border-bottom: 20px solid transparent;
       border-bottom: 20px solid transparent;
       border-left: 20px solid transparent;
       border-left: 20px solid transparent;
       border-left-width: 0;
       border-left-width: 0;
@@ -65,7 +65,7 @@
     .page-comment-main {
     .page-comment-main {
       @extend %comment-section;
       @extend %comment-section;
       margin-left: 4.5em;
       margin-left: 4.5em;
-      background: #e6e9ec;
+      background: $gray-200;
       border-radius: 0.35em;
       border-radius: 0.35em;
     }
     }
 
 

+ 4 - 4
src/client/styles/scss/_editor-attachment.scss

@@ -22,7 +22,7 @@
         background: rgba(200, 200, 200, 0.8);
         background: rgba(200, 200, 200, 0.8);
 
 
         .overlay-content {
         .overlay-content {
-          color: #444;
+          color: $gray-700;
         }
         }
       }
       }
     }
     }
@@ -51,7 +51,7 @@
       // accepted
       // accepted
       &.dropzone-accepted:not(.dropzone-rejected) {
       &.dropzone-accepted:not(.dropzone-rejected) {
         .overlay.overlay-dropzone-active {
         .overlay.overlay-dropzone-active {
-          border: 4px dashed #ccc;
+          border: 4px dashed $gray-300;
 
 
           .overlay-content {
           .overlay-content {
             // insert content
             // insert content
@@ -62,7 +62,7 @@
             }
             }
 
 
             // style
             // style
-            color: #666;
+            color: $secondary;
             background: rgba(200, 200, 200, 0.8);
             background: rgba(200, 200, 200, 0.8);
           }
           }
         }
         }
@@ -106,7 +106,7 @@
     padding-bottom: 3px;
     padding-bottom: 3px;
     font-size: small;
     font-size: small;
     border: none;
     border: none;
-    border-top: 1px dotted #ccc;
+    border-top: 1px dotted $gray-300;
     border-bottom: none;
     border-bottom: none;
 
 
     &:active {
     &:active {

+ 1 - 1
src/client/styles/scss/_editor-overlay.scss

@@ -4,7 +4,7 @@
     .overlay-content {
     .overlay-content {
       padding: $contentPadding;
       padding: $contentPadding;
       font-size: $contentFontSize;
       font-size: $contentFontSize;
-      color: #444;
+      color: $gray-700;
       background: rgba(200, 200, 200, 0.5);
       background: rgba(200, 200, 200, 0.5);
     }
     }
   }
   }

+ 4 - 4
src/client/styles/scss/_hljs.scss

@@ -15,8 +15,8 @@ pre.hljs {
     padding: 0 4px;
     padding: 0 4px;
     font-style: normal;
     font-style: normal;
     font-weight: bold;
     font-weight: bold;
-    color: #333;
-    background: #ccc;
+    color: $gray-900;
+    background: $gray-300;
     opacity: 0.6;
     opacity: 0.6;
   }
   }
 }
 }
@@ -24,12 +24,12 @@ pre.hljs {
 // styles for highlightjs-line-numbers
 // styles for highlightjs-line-numbers
 .hljs-ln td.hljs-ln-numbers {
 .hljs-ln td.hljs-ln-numbers {
   padding-right: 5px;
   padding-right: 5px;
-  color: #ccc;
+  color: $gray-300;
 
 
   text-align: center;
   text-align: center;
   vertical-align: top;
   vertical-align: top;
   user-select: none;
   user-select: none;
-  border-right: 1px solid #ccc;
+  border-right: 1px solid $gray-300;
 }
 }
 
 
 .hljs-ln td.hljs-ln-code {
 .hljs-ln td.hljs-ln-code {

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

@@ -94,10 +94,10 @@ body {
   }
   }
   .main {
   .main {
     header {
     header {
-      border-bottom: solid 1px #666;
+      border-bottom: solid 1px $secondary;
       h1 {
       h1 {
         font-size: 2em;
         font-size: 2em;
-        color: #000;
+        color: black;
       }
       }
     }
     }
 
 
@@ -110,7 +110,7 @@ body {
       max-width: 100%;
       max-width: 100%;
       margin-bottom: 20px;
       margin-bottom: 20px;
       font-size: 0.9em;
       font-size: 0.9em;
-      border: solid 1px #aaa;
+      border: solid 1px $gray-400;
 
 
       .revision-toc-head {
       .revision-toc-head {
         display: inline-block;
         display: inline-block;
@@ -125,8 +125,8 @@ body {
 
 
     .meta {
     .meta {
       margin-top: 32px;
       margin-top: 32px;
-      color: #666;
-      border-top: solid 1px #ccc;
+      color: $secondary;
+      border-top: solid 1px $gray-300;
     }
     }
   }
   }
 }
 }

+ 7 - 7
src/client/styles/scss/_login.scss

@@ -101,31 +101,31 @@
     ),
     ),
     'google': (
     'google': (
       rgba(#24292e, 0.4),
       rgba(#24292e, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'github': (
     'github': (
       rgba(lighten(black, 20%), 0.4),
       rgba(lighten(black, 20%), 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'facebook': (
     'facebook': (
       rgba(#29487d, 0.4),
       rgba(#29487d, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'twitter': (
     'twitter': (
       rgba(#1da1f2, 0.4),
       rgba(#1da1f2, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'oidc': (
     'oidc': (
       rgba(#24292e, 0.4),
       rgba(#24292e, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'saml': (
     'saml': (
       rgba(#55a79a, 0.4),
       rgba(#55a79a, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
     'basic': (
     'basic': (
       rgba(#24292e, 0.4),
       rgba(#24292e, 0.4),
-      #444,
+      $gray-700,
     ),
     ),
   );
   );
 
 

+ 1 - 1
src/client/styles/scss/_navbar_kibela.scss

@@ -4,7 +4,7 @@
   .grw-navbar {
   .grw-navbar {
     height: 60px;
     height: 60px;
     background: white;
     background: white;
-    border-bottom: solid 1px #e6e9ec;
+    border-bottom: solid 1px $gray-200;
     .navbar-nav {
     .navbar-nav {
       .confidential {
       .confidential {
         color: white;
         color: white;

+ 7 - 6
src/client/styles/scss/_on-edit.scss

@@ -187,7 +187,8 @@ body.on-edit {
       }
       }
 
 
       // add icon on cursor
       // add icon on cursor
-      .markdown-table-activated, .markdown-link-activated {
+      .markdown-table-activated,
+      .markdown-link-activated {
         .CodeMirror-cursor {
         .CodeMirror-cursor {
           &:after {
           &:after {
             position: relative;
             position: relative;
@@ -197,7 +198,7 @@ body.on-edit {
             width: 1em;
             width: 1em;
             height: 1em;
             height: 1em;
             content: ' ';
             content: ' ';
-  
+
             background-repeat: no-repeat;
             background-repeat: no-repeat;
             background-size: 1em;
             background-size: 1em;
           }
           }
@@ -310,12 +311,12 @@ body.on-edit {
 
 
 #tag-edit-button-tooltip {
 #tag-edit-button-tooltip {
   .tooltip-inner {
   .tooltip-inner {
-    color: #000;
-    background-color: #fff;
-    border: 1px solid #ccc;
+    color: black;
+    background-color: white;
+    border: 1px solid $gray-300;
   }
   }
 
 
   .tooltip-arrow {
   .tooltip-arrow {
-    border-bottom: 5px solid #ccc;
+    border-bottom: 5px solid $gray-300;
   }
   }
 }
 }

+ 1 - 1
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -79,7 +79,7 @@ $alert-color-level: -10;
 //== Progress bar
 //== Progress bar
 $progress-height: 4px;
 $progress-height: 4px;
 $progress-border-radius: $border-radius-sm;
 $progress-border-radius: $border-radius-sm;
-$progress-bg: #f0f0f0;
+$progress-bg: $gray-100;
 $progress-box-shadow: none;
 $progress-box-shadow: none;
 
 
 //== Code
 //== Code

+ 4 - 4
src/client/styles/scss/_page.scss

@@ -4,7 +4,7 @@
 .main-container {
 .main-container {
   .url-line {
   .url-line {
     font-size: 1rem;
     font-size: 1rem;
-    color: #999;
+    color: $gray-400;
   }
   }
 
 
   h1.title {
   h1.title {
@@ -17,7 +17,7 @@
 
 
     // crowi layout only
     // crowi layout only
     a.last-path {
     a.last-path {
-      color: #ccc;
+      color: $gray-300;
 
 
       &:hover {
       &:hover {
         color: inherit;
         color: inherit;
@@ -65,7 +65,7 @@
 
 
       .revision-history-diff {
       .revision-history-diff {
         padding-left: 40px;
         padding-left: 40px;
-        color: #333;
+        color: $gray-900;
         table-layout: fixed;
         table-layout: fixed;
       }
       }
     }
     }
@@ -171,7 +171,7 @@
   left: 5%;
   left: 5%;
   width: 90%;
   width: 90%;
   height: 90%;
   height: 90%;
-  background: #000;
+  background: black;
 
 
   iframe {
   iframe {
     width: 100%;
     width: 100%;

+ 3 - 3
src/client/styles/scss/_page_list.scss

@@ -54,7 +54,7 @@ body .page-list {
 .popular-page-high {
 .popular-page-high {
   font-size: 1.1em;
   font-size: 1.1em;
   font-weight: bold;
   font-weight: bold;
-  color: #e80000;
+  color: darken($red, 5%);
 }
 }
 
 
 .popular-page-mid {
 .popular-page-mid {
@@ -67,8 +67,8 @@ body .page-list {
 }
 }
 
 
 .card-timeline {
 .card-timeline {
-  border: 1px solid #ccc;
+  border: 1px solid $gray-300;
   > .card-header {
   > .card-header {
-    background-color: #ccc;
+    background-color: $gray-300;
   }
   }
 }
 }

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

@@ -1,6 +1,6 @@
 .search-listpage-icon {
 .search-listpage-icon {
   font-size: 16px;
   font-size: 16px;
-  color: #999;
+  color: $gray-400;
 }
 }
 
 
 .search-listpage-clear {
 .search-listpage-clear {
@@ -11,7 +11,7 @@
   height: 22px;
   height: 22px;
   padding: 8px;
   padding: 8px;
   font-size: 0.6em;
   font-size: 0.6em;
-  color: #ccc;
+  color: $gray-300;
 }
 }
 
 
 .search-typeahead {
 .search-typeahead {
@@ -26,7 +26,7 @@
     width: 24px;
     width: 24px;
     height: 24px;
     height: 24px;
     padding: 0;
     padding: 0;
-    color: #999;
+    color: $gray-400;
   }
   }
 
 
   .rbt-menu {
   .rbt-menu {
@@ -48,7 +48,7 @@
 
 
       .page-list-meta {
       .page-list-meta {
         font-size: 0.9em;
         font-size: 0.9em;
-        color: #999;
+        color: $gray-400;
 
 
         > span {
         > span {
           margin-right: 0.3rem;
           margin-right: 0.3rem;
@@ -213,7 +213,7 @@
       .wiki {
       .wiki {
         padding: 16px;
         padding: 16px;
         font-size: 13px;
         font-size: 13px;
-        border: solid 1px #ccc;
+        border: solid 1px $gray-300;
       }
       }
     }
     }
   }
   }

+ 3 - 3
src/client/styles/scss/_shortcuts.scss

@@ -30,15 +30,15 @@
     margin: 0px 4px;
     margin: 0px 4px;
     /*Text Properties*/
     /*Text Properties*/
     font: 18px/36px Helvetica, serif;
     font: 18px/36px Helvetica, serif;
-    color: #666;
+    color: $secondary;
     text-align: center;
     text-align: center;
     text-transform: uppercase;
     text-transform: uppercase;
-    background: #fff;
+    background: white;
     border-radius: 4px;
     border-radius: 4px;
     box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.5);
     box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.5);
     /* SVG Properties*/
     /* SVG Properties*/
     polygon {
     polygon {
-      fill: #666;
+      fill: $secondary;
     }
     }
 
 
     &.key-longer {
     &.key-longer {

+ 1 - 1
src/client/styles/scss/_subnav.scss

@@ -56,7 +56,7 @@
     .picture {
     .picture {
       width: 22px;
       width: 22px;
       height: 22px;
       height: 22px;
-      border: 1px solid #ccc;
+      border: 1px solid $gray-300;
 
 
       &.picture-xs {
       &.picture-xs {
         width: 14px;
         width: 14px;

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

@@ -16,7 +16,7 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   .user-page-name {
   .user-page-name {
     margin: 0;
     margin: 0;
     font-size: 2.5em;
     font-size: 2.5em;
-    color: #666;
+    color: $secondary;
   }
   }
 
 
   .picture {
   .picture {
@@ -26,7 +26,7 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
 
 
   div.user-page-meta {
   div.user-page-meta {
     padding-left: 0;
     padding-left: 0;
-    color: #999;
+    color: $gray-400;
 
 
     .user-page-username {
     .user-page-username {
       font-weight: bold;
       font-weight: bold;

+ 1 - 0
src/client/styles/scss/_wiki.scss

@@ -67,6 +67,7 @@ div.body {
     margin: 0 0 30px 0;
     margin: 0 0 30px 0;
     font-size: 0.9em;
     font-size: 0.9em;
     color: lighten($gray-800, 35%);
     color: lighten($gray-800, 35%);
+    border-left: 0.3rem solid #ddd;
   }
   }
 
 
   img {
   img {

+ 16 - 9
src/client/styles/scss/atoms/_buttons.scss

@@ -1,14 +1,21 @@
-.btn.btn-outline-info.btn-like,
-.btn.btn-outline-warning.btn-bookmark {
-  color: $secondary;
-
-  &.active,
-  &:hover {
-    // header buttons are always white for active
-    color: white !important;
+.btn.btn-like {
+  @include button-outline-variant($secondary, lighten($info, 15%), rgba(lighten($info, 10%), 0.5), rgba(lighten($info, 10%), 0.5));
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active {
+    color: lighten($info, 15%);
+  }
+  &:not(:disabled):not(.disabled):not(:hover) {
+    background-color: transparent;
   }
   }
+}
 
 
-  &:not(:hover):not(.active) {
+.btn.btn-bookmark {
+  @include button-outline-variant($secondary, $warning, rgba(lighten($warning, 20%), 0.5), rgba(lighten($warning, 20%), 0.5));
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active {
+    color: $warning;
+  }
+  &:not(:disabled):not(.disabled):not(:hover) {
     background-color: transparent;
     background-color: transparent;
   }
   }
 }
 }

+ 5 - 5
src/client/styles/scss/style-presentation.scss

@@ -94,14 +94,14 @@
           > td {
           > td {
             padding: 1em;
             padding: 1em;
             vertical-align: top;
             vertical-align: top;
-            border-top: 1px solid #999;
+            border-top: 1px solid $gray-400;
           }
           }
         }
         }
       }
       }
       // Bottom align for column headings
       // Bottom align for column headings
       > thead > tr > th {
       > thead > tr > th {
         vertical-align: bottom;
         vertical-align: bottom;
-        border-bottom: 2px solid #888;
+        border-bottom: 2px solid $gray-500;
       }
       }
       // Remove top border from thead by default
       // Remove top border from thead by default
       > caption + thead,
       > caption + thead,
@@ -116,18 +116,18 @@
       }
       }
       // Account for multiple tbody instances
       // Account for multiple tbody instances
       > tbody + tbody {
       > tbody + tbody {
-        border-top: 2px solid #888;
+        border-top: 2px solid $gray-500;
       }
       }
 
 
       // .table-bordered
       // .table-bordered
-      border: 1px solid #999;
+      border: 1px solid $gray-400;
       > thead,
       > thead,
       > tbody,
       > tbody,
       > tfoot {
       > tfoot {
         > tr {
         > tr {
           > th,
           > th,
           > td {
           > td {
-            border: 1px solid #999;
+            border: 1px solid $gray-400;
           }
           }
         }
         }
       }
       }

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

@@ -16,7 +16,7 @@ $color-tags: #949494 !default;
 $bgcolor-tags: $dark !default;
 $bgcolor-tags: $dark !default;
 
 
 // override bootstrap variables
 // override bootstrap variables
-$border-color: #444;
+$border-color: $gray-700;
 $table-dark-color: $color-table;
 $table-dark-color: $color-table;
 $table-dark-bg: $bgcolor-table;
 $table-dark-bg: $bgcolor-table;
 $table-dark-border-color: $border-color-table;
 $table-dark-border-color: $border-color-table;
@@ -125,7 +125,7 @@ ul.pagination {
   .input-group {
   .input-group {
     .input-group-text {
     .input-group-text {
       color: darken(white, 30%);
       color: darken(white, 30%);
-      background-color: rgba(#444, 0.7);
+      background-color: rgba($gray-700, 0.7);
     }
     }
 
 
     .form-control {
     .form-control {
@@ -141,10 +141,10 @@ ul.pagination {
 
 
   .btn-fill {
   .btn-fill {
     .btn-label {
     .btn-label {
-      color: #ccc;
+      color: $gray-300;
     }
     }
     .btn-label-text {
     .btn-label-text {
-      color: #aaa;
+      color: $gray-400;
     }
     }
   }
   }
 
 
@@ -180,7 +180,7 @@ ul.pagination {
  */
  */
 .grw-drawer-toggler {
 .grw-drawer-toggler {
   @extend .btn-dark;
   @extend .btn-dark;
-  color: #999;
+  color: $gray-400;
 }
 }
 
 
 /*
 /*

+ 2 - 2
src/client/styles/scss/theme/_apply-colors-kibela.scss

@@ -60,7 +60,7 @@ body.kibela {
         background-color: transparent;
         background-color: transparent;
 
 
         &:hover {
         &:hover {
-          background: #eee;
+          background: $gray-100;
         }
         }
       }
       }
 
 
@@ -124,7 +124,7 @@ body.kibela {
 
 
     /* Modal */
     /* Modal */
     .modal-title {
     .modal-title {
-      color: #ffffff; // override header colors
+      color: white; // override header colors
     }
     }
     .modal-content {
     .modal-content {
       background-color: $themelight;
       background-color: $themelight;

+ 7 - 7
src/client/styles/scss/theme/antarctic.scss

@@ -49,8 +49,8 @@ html[dark] {
 
 
   // Background colors
   // Background colors
   $bgcolor-global: $themelight;
   $bgcolor-global: $themelight;
-  $bgcolor-inline-code: #f0f0f0; //optional
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $gray-50;
 
 
   // Font colors
   // Font colors
   $color-global: black;
   $color-global: black;
@@ -81,7 +81,7 @@ html[dark] {
 
 
   // Sidebar
   // Sidebar
   $bgcolor-sidebar: $themecolor;
   $bgcolor-sidebar: $themecolor;
-  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37); // optional
+  $bgcolor-sidebar-nav-item-active: rgba(black, 0.37); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
   // Sidebar resize button
   $color-resize-button: $color-reversal;
   $color-resize-button: $color-reversal;
@@ -98,7 +98,7 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: $gray-300; // former: `$navbar-border: $gray-300;`
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors
@@ -151,7 +151,7 @@ html[dark] {
 
 
 //   // Font colors
 //   // Font colors
 //   $color-global: #eeeeee;
 //   $color-global: #eeeeee;
-//   $color-reversal: #333333;
+//   $color-reversal: $gray-900;
 //   // $color-header: desaturate($primary, 20%);
 //   // $color-header: desaturate($primary, 20%);
 //   $color-link: $primary;
 //   $color-link: $primary;
 //   $color-link-hover: lighten($color-link, 10%);
 //   $color-link-hover: lighten($color-link, 10%);
@@ -169,13 +169,13 @@ html[dark] {
 
 
 //   // Logo colors
 //   // Logo colors
 //   $bgcolor-logo: $bgcolor-navbar;
 //   $bgcolor-logo: $bgcolor-navbar;
-//   $fillcolor-logo-mark: #444;
+//   $fillcolor-logo-mark: $gray-700;
 
 
 //   // Icon colors
 //   // Icon colors
 //   $color-editor-icons: darken($accentcolor, 15%);
 //   $color-editor-icons: darken($accentcolor, 15%);
 
 
 //   // Border colors
 //   // Border colors
-//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
+//   $border-color-theme: black; // former: `$navbar-border: $gray-300;`
 
 
 //   // Dropdown colors
 //   // Dropdown colors
 //   $bgcolor-dropdown-link-active: $primary;
 //   $bgcolor-dropdown-link-active: $primary;

+ 9 - 9
src/client/styles/scss/theme/christmas.scss

@@ -19,7 +19,7 @@ $subthemecolor: #30882c;
 $bgcolor-global: $themelight;
 $bgcolor-global: $themelight;
 $linktext: $subthemecolor;
 $linktext: $subthemecolor;
 $linktext-hover: lighten($subthemecolor, 15%);
 $linktext-hover: lighten($subthemecolor, 15%);
-$sidebar-text: #ffffff;
+$sidebar-text: white;
 $fillcolor-logo-mark: lighten(desaturate($themecolor, 50%), 50%);
 $fillcolor-logo-mark: lighten(desaturate($themecolor, 50%), 50%);
 $color-link-wiki: lighten($subthemecolor, 5%);
 $color-link-wiki: lighten($subthemecolor, 5%);
 $color-link-wiki-hover: lighten($color-link-wiki, 15%);
 $color-link-wiki-hover: lighten($color-link-wiki, 15%);
@@ -41,19 +41,19 @@ html[light],
 html[dark] {
 html[dark] {
   $primary: #d3c665;
   $primary: #d3c665;
   // Background colors
   // Background colors
-  $bgcolor-card: #f5f5f5;
-  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-card: $gray-50;
+  $bgcolor-inline-code: $gray-100; //optional
 
 
   // Font colors
   // Font colors
   $color-global: #112744;
   $color-global: #112744;
-  $color-reversal: #eee;
+  $color-reversal: $gray-100;
   $color-link: $subthemecolor;
   $color-link: $subthemecolor;
   $color-link-hover: lighten($color-link, 10%);
   $color-link-hover: lighten($color-link, 10%);
   $color-link-nabvar: $color-reversal;
   $color-link-nabvar: $color-reversal;
   $color-inline-code: #c7254e; // optional
   $color-inline-code: #c7254e; // optional
 
 
   // Table colors
   // Table colors
-  $border-color-table: #aaa; // optional
+  $border-color-table: $gray-400; // optional
 
 
   // List Group colors
   // List Group colors
   // $color-list: $color-global;
   // $color-list: $color-global;
@@ -74,7 +74,7 @@ html[dark] {
 
 
   // Sidebar
   // Sidebar
   $bgcolor-sidebar: $subthemecolor;
   $bgcolor-sidebar: $subthemecolor;
-  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37); // optional
+  $bgcolor-sidebar-nav-item-active: rgba(black, 0.37); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
   // Sidebar resize button
   // Sidebar resize button
   $color-resize-button: $color-reversal;
   $color-resize-button: $color-reversal;
@@ -90,7 +90,7 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: $gray-300; // former: `$navbar-border: $gray-300;`
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors
@@ -136,11 +136,11 @@ html[dark] {
   .nologin {
   .nologin {
     .input-group {
     .input-group {
       .input-group-text {
       .input-group-text {
-        color: #444;
+        color: $gray-700;
         background-color: rgba(darken(white, 20%), 0.6);
         background-color: rgba(darken(white, 20%), 0.6);
       }
       }
       .form-control {
       .form-control {
-        color: #444;
+        color: $gray-700;
         background-color: rgba(white, 0.6);
         background-color: rgba(white, 0.6);
       }
       }
     }
     }

+ 5 - 5
src/client/styles/scss/theme/default.scss

@@ -117,7 +117,7 @@ html[dark] {
 
 
   // Font colors
   // Font colors
   $color-global: #a8a8a8;
   $color-global: #a8a8a8;
-  $color-reversal: #333333;
+  $color-reversal: $gray-900;
   // $color-header: desaturate($primary, 20%);
   // $color-header: desaturate($primary, 20%);
   $color-link: #7b9ad5;
   $color-link: #7b9ad5;
   $color-link-hover: lighten($color-link, 10%);
   $color-link-hover: lighten($color-link, 10%);
@@ -148,7 +148,7 @@ html[dark] {
 
 
   // Logo colors
   // Logo colors
   $bgcolor-logo: $bgcolor-navbar;
   $bgcolor-logo: $bgcolor-navbar;
-  $fillcolor-logo-mark: #444;
+  $fillcolor-logo-mark: $gray-700;
 
 
   // Sidebar
   // Sidebar
   $bgcolor-sidebar: #122c55;
   $bgcolor-sidebar: #122c55;
@@ -169,7 +169,7 @@ html[dark] {
   $bgcolor-subnav: lighten($bgcolor-global, 4%); // optional
   $bgcolor-subnav: lighten($bgcolor-global, 4%); // optional
 
 
   // Tabs
   // Tabs
-  $bordercolor-nav-tabs: #444; // optional
+  $bordercolor-nav-tabs: $gray-700; // optional
   // $color-nav-tabs-link-active: #; //optional
   // $color-nav-tabs-link-active: #; //optional
   $bordercolor-nav-tabs-hover: #666 #666 $bordercolor-nav-tabs; // optional
   $bordercolor-nav-tabs-hover: #666 #666 $bordercolor-nav-tabs; // optional
   // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
   // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
@@ -182,8 +182,8 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #444;
-  $bordercolor-inline-code: #666; // optional
+  $border-color-theme: $gray-700;
+  $bordercolor-inline-code: $secondary; // optional
 
 
   // Dropdown colors
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;
   $bgcolor-dropdown-link-active: $primary;

+ 1 - 1
src/client/styles/scss/theme/future.scss

@@ -14,7 +14,7 @@ html[dark] {
 
 
   // Font colors
   // Font colors
   $color-global: #95abba;
   $color-global: #95abba;
-  $color-reversal: #222;
+  $color-reversal: $gray-900;
   $color-header: #95abba;
   $color-header: #95abba;
   $color-link: $accentcolor;
   $color-link: $accentcolor;
   $color-link-hover: lighten($color-link, 20%);
   $color-link-hover: lighten($color-link, 20%);

+ 3 - 3
src/client/styles/scss/theme/halloween.scss

@@ -38,7 +38,7 @@ html[dark] {
   // Background colors
   // Background colors
   $bgcolor-global: #050000;
   $bgcolor-global: #050000;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-inline-code: #1f1f22; //optional
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-card: $gray-50;
 
 
   // Font colors
   // Font colors
   $color-global: #e9af2b;
   $color-global: #e9af2b;
@@ -93,7 +93,7 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: $gray-300; // former: `$navbar-border: $gray-300;`
   $bordercolor-inline-code: #4d4d4d; // optional
   $bordercolor-inline-code: #4d4d4d; // optional
 
 
   // Dropdown colors
   // Dropdown colors
@@ -114,6 +114,6 @@ html[dark] {
 
 
   pre {
   pre {
     color: #edba4a;
     color: #edba4a;
-    background: #000000;
+    background: black;
   }
   }
 }
 }

+ 5 - 5
src/client/styles/scss/theme/island.scss

@@ -8,9 +8,9 @@ html[light],
 html[dark] {
 html[dark] {
   $primary: $color-primary;
   $primary: $color-primary;
   // Background colors
   // Background colors
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-card: $gray-50;
   $bgcolor-global: lighten($color-themelight, 10%);
   $bgcolor-global: lighten($color-themelight, 10%);
-  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-inline-code: $gray-100; //optional
 
 
   // Font colors
   // Font colors
   $color-global: #112744;
   $color-global: #112744;
@@ -49,7 +49,7 @@ html[dark] {
 
 
   // Sidebar
   // Sidebar
   $bgcolor-sidebar: #0d3955;
   $bgcolor-sidebar: #0d3955;
-  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37);
+  $bgcolor-sidebar-nav-item-active: rgba(black, 0.37);
   // $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
   // $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
   // Sidebar resize button
@@ -64,13 +64,13 @@ html[dark] {
   $bgcolor-sidebar-list-group: #eff8f7; // optional
   $bgcolor-sidebar-list-group: #eff8f7; // optional
 
 
   // Tabs
   // Tabs
-  $bordercolor-nav-tabs: #ccc; // optional
+  $bordercolor-nav-tabs: $gray-300; // optional
 
 
   // Icon colors
   // Icon colors
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc;
+  $border-color-theme: $gray-300;
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors

+ 1 - 1
src/client/styles/scss/theme/kibela.scss

@@ -20,7 +20,7 @@ html[dark] {
   $color-link: rgb(74, 109, 204);
   $color-link: rgb(74, 109, 204);
   $color-link-hover: lighten($color-link, 12%);
   $color-link-hover: lighten($color-link, 12%);
   $sidebar-text: $bgcolor-theme;
   $sidebar-text: $bgcolor-theme;
-  $color-reversal: #eee;
+  $color-reversal: $gray-100;
 
 
   $primary: $bgcolor-theme;
   $primary: $bgcolor-theme;
   $info: lighten($bgcolor-theme, 20%);
   $info: lighten($bgcolor-theme, 20%);

+ 4 - 4
src/client/styles/scss/theme/mono-blue.scss

@@ -12,12 +12,12 @@ html[light] {
 
 
   // Background colors
   // Background colors
   $bgcolor-global: $themelight;
   $bgcolor-global: $themelight;
-  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: darken($themelight, 5%);
   $bgcolor-card: darken($themelight, 5%);
 
 
   // Font colors
   // Font colors
   $color-global: $themecolor;
   $color-global: $themecolor;
-  $color-reversal: #eee;
+  $color-reversal: $gray-100;
   $color-link: lighten($primary, 5%);
   $color-link: lighten($primary, 5%);
   $color-link-hover: lighten($color-link, 12%);
   $color-link-hover: lighten($color-link, 12%);
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki: lighten($primary, 20%);
@@ -62,7 +62,7 @@ html[light] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc;
+  $border-color-theme: $gray-300;
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors
@@ -108,7 +108,7 @@ html[dark] {
 
 
   // Font colors
   // Font colors
   $color-global: #d3d4d4;
   $color-global: #d3d4d4;
-  $color-reversal: #eee;
+  $color-reversal: $gray-100;
   $color-link: #97d1f0;
   $color-link: #97d1f0;
   $color-link-hover: darken($color-link, 12%);
   $color-link-hover: darken($color-link, 12%);
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki: lighten($primary, 20%);

+ 4 - 4
src/client/styles/scss/theme/nature.scss

@@ -38,11 +38,11 @@ $themecolor: #12b105;
 html[light],
 html[light],
 html[dark] {
 html[dark] {
   $primary: #460039;
   $primary: #460039;
-  $light: #f0f0f0;
+  $light: $gray-100;
 
 
   // Background colors
   // Background colors
   $bgcolor-global: #fdfdfd;
   $bgcolor-global: #fdfdfd;
-  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: #f1ffe4;
   $bgcolor-card: #f1ffe4;
   $bgcolor-subnav: #fafafa;
   $bgcolor-subnav: #fafafa;
 
 
@@ -79,7 +79,7 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc;
+  $border-color-theme: $gray-300;
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors
@@ -88,7 +88,7 @@ html[dark] {
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
   // Table colors
   // Table colors
-  $border-color-table: #aaa; // optional
+  $border-color-table: $gray-400; // optional
 
 
   // admin theme box
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
   $color-theme-color-box: lighten($primary, 20%);

+ 3 - 3
src/client/styles/scss/theme/spring.scss

@@ -32,8 +32,8 @@ html[dark] {
 
 
   // Background colors
   // Background colors
   $bgcolor-global: white;
   $bgcolor-global: white;
-  $bgcolor-inline-code: #f0f0f0; //optional
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $gray-50;
 
 
   // Font colors
   // Font colors
   $color-global: black;
   $color-global: black;
@@ -79,7 +79,7 @@ html[dark] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: $gray-300; // former: `$navbar-border: $gray-300;`
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors

+ 4 - 4
src/client/styles/scss/theme/wood.scss

@@ -41,7 +41,7 @@ html[dark] {
   $primary: #aaa45f;
   $primary: #aaa45f;
 
 
   // Background colors
   // Background colors
-  $bgcolor-global: #ffffff;
+  $bgcolor-global: white;
   $bgcolor-card: #ece8de;
   $bgcolor-card: #ece8de;
 
 
   // Font colors
   // Font colors
@@ -63,7 +63,7 @@ html[dark] {
   // List Group colors
   // List Group colors
   // $color-list: $color-global;
   // $color-list: $color-global;
   $bgcolor-list: transparent;
   $bgcolor-list: transparent;
-  $color-list-hover: #eee;
+  $color-list-hover: $gray-100;
   $bgcolor-list-hover: darken($bgcolor-global, 3%);
   $bgcolor-list-hover: darken($bgcolor-global, 3%);
   // $color-list-active: $color-reversal;
   // $color-list-active: $color-reversal;
   // $bgcolor-list-active: $primary;
   // $bgcolor-list-active: $primary;
@@ -71,7 +71,7 @@ html[dark] {
   // Table colors
   // Table colors
   // $color-table: #; // optional
   // $color-table: #; // optional
   // $bgcolor-table: #; // optional
   // $bgcolor-table: #; // optional
-  $border-color-table: #aaa; // optional
+  $border-color-table: $gray-400; // optional
   // $color-table-hover: #; // optional
   // $color-table-hover: #; // optional
   // $bgcolor-table-hover: #; // optional
   // $bgcolor-table-hover: #; // optional
 
 
@@ -97,7 +97,7 @@ html[dark] {
   $bgcolor-resize-button: $themecolor;
   $bgcolor-resize-button: $themecolor;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: $gray-300; // former: `$navbar-border: $gray-300;`
   $bordercolor-inline-code: #ccc8c8; // optional
   $bordercolor-inline-code: #ccc8c8; // optional
 
 
   // Dropdown colors
   // Dropdown colors

+ 5 - 5
src/linter-checker/test.scss

@@ -2,7 +2,7 @@
  * VSCode の Stylelint 設定チェック方法
  * VSCode の Stylelint 設定チェック方法
  *
  *
  * 1. .stylelintrc.json ファイル中の `src/linter-checker/test.scss` 行を削除
  * 1. .stylelintrc.json ファイル中の `src/linter-checker/test.scss` 行を削除
- * 
+ *
  * 2. VSCode で以下のエラーが表示されていることを確認
  * 2. VSCode で以下のエラーが表示されていることを確認
  *   - color で stylelint(order/properties-order)
  *   - color で stylelint(order/properties-order)
  *   - ul で stylelint(selector-combinator-space-after)
  *   - ul で stylelint(selector-combinator-space-after)
@@ -16,10 +16,10 @@
  */
  */
 
 
 .test {
 .test {
-  background: #ccc;
-  color: #333;
+  background: $gray-300;
+  color: $gray-900;
 
 
-  ul>li {
+  ul > li {
     margin-left: 0;
     margin-left: 0;
   }
   }
-}
+}

+ 7 - 1
src/server/middlewares/access-token-parser.js

@@ -16,6 +16,12 @@ module.exports = (crowi) => {
     logger.debug('accessToken is', accessToken);
     logger.debug('accessToken is', accessToken);
 
 
     const user = await User.findUserByApiToken(accessToken);
     const user = await User.findUserByApiToken(accessToken);
+
+    if (user == null) {
+      logger.debug('The access token is invalid');
+      return next();
+    }
+
     // transforming attributes
     // transforming attributes
     // see User model
     // see User model
     req.user = user.toObject();
     req.user = user.toObject();
@@ -23,7 +29,7 @@ module.exports = (crowi) => {
 
 
     logger.debug('Access token parsed: skipCsrfVerify');
     logger.debug('Access token parsed: skipCsrfVerify');
 
 
-    next();
+    return next();
   };
   };
 
 
 };
 };

+ 191 - 11
src/server/routes/apiv3/pages.js

@@ -16,6 +16,92 @@ const router = express.Router();
  *  tags:
  *  tags:
  *    name: Pages
  *    name: Pages
  */
  */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Page:
+ *        description: Page
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          __v:
+ *            type: number
+ *            description: DB record version
+ *            example: 0
+ *          commentCount:
+ *            type: number
+ *            description: count of comments
+ *            example: 3
+ *          createdAt:
+ *            type: string
+ *            description: date created at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          creator:
+ *            $ref: '#/components/schemas/User'
+ *          extended:
+ *            type: object
+ *            description: extend data
+ *            example: {}
+ *          grant:
+ *            type: number
+ *            description: grant
+ *            example: 1
+ *          grantedUsers:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: ["5ae5fccfc5577b0004dbd8ab"]
+ *          lastUpdateUser:
+ *            $ref: '#/components/schemas/User'
+ *          liker:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: []
+ *          path:
+ *            type: string
+ *            description: page path
+ *            example: /
+ *          redirectTo:
+ *            type: string
+ *            description: redirect path
+ *            example: ""
+ *          revision:
+ *            type: string
+ *            description: revision ID
+ *            example: ["5ae5fccfc5577b0004dbd8ab"]
+ *          seenUsers:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: ["5ae5fccfc5577b0004dbd8ab"]
+ *          status:
+ *            type: string
+ *            description: status
+ *            enum:
+ *              - 'wip'
+ *              - 'published'
+ *              - 'deleted'
+ *              - 'deprecated'
+ *            example: published
+ *          updatedAt:
+ *            type: string
+ *            description: date updated at
+ *            example: 2010-01-01T00:00:00.000Z
+ */
+
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
@@ -48,7 +134,41 @@ module.exports = (crowi) => {
     ],
     ],
   };
   };
 
 
-  // TODO write swagger(GW-3384)
+  /**
+   * @swagger
+   *
+   *    /pages/create:
+   *      post:
+   *        tags: [Pages]
+   *        operationId: createPage
+   *        description: Create page
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  body:
+   *                    type: string
+   *                    description: Text of page
+   *                  path:
+   *                    $ref: '#/components/schemas/Page/properties/path'
+   *                  grant:
+   *                    $ref: '#/components/schemas/Page/properties/grant'
+   *                required:
+   *                  - body
+   *                  - path
+   *        responses:
+   *          200:
+   *            description: Succeeded to create page.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *          409:
+   *            description: page path is already existed
+   */
   router.post('/', accessTokenParser, loginRequiredStrictly, csrf, validator.createPage, apiV3FormValidator, async(req, res) => {
   router.post('/', accessTokenParser, loginRequiredStrictly, csrf, validator.createPage, apiV3FormValidator, async(req, res) => {
     const {
     const {
       body, grant, grantUserGroupId, pageTags, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, socketClientId,
       body, grant, grantUserGroupId, pageTags, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, socketClientId,
@@ -62,8 +182,7 @@ module.exports = (crowi) => {
     // check page existence
     // check page existence
     const isExist = await Page.count({ path }) > 0;
     const isExist = await Page.count({ path }) > 0;
     if (isExist) {
     if (isExist) {
-      res.code = 'page_exists';
-      return res.apiv3Err('Page exists', 409);
+      return res.apiv3Err(new ErrorV3('Failed to post page', 'page_exists'), 500);
     }
     }
 
 
     const options = { socketClientId };
     const options = { socketClientId };
@@ -149,12 +268,77 @@ module.exports = (crowi) => {
       return res.apiv3(result);
       return res.apiv3(result);
     }
     }
     catch (err) {
     catch (err) {
-      res.code = 'unknown';
       logger.error('Failed to get recent pages', err);
       logger.error('Failed to get recent pages', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
+    }
+  });
+
+  // TODO write swagger(GW-3430) and add validation (GW-3429)
+  router.put('/rename', accessTokenParser, loginRequiredStrictly, csrf, async(req, res) => {
+    const { pageId, isRecursively, revisionId } = req.body;
+
+    let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
+
+    const options = {
+      createRedirectPage: req.body.isRenameRedirect,
+      updateMetadata: req.body.isRemainMetadata,
+      socketClientId: +req.body.socketClientId || undefined,
+    };
+
+    if (!Page.isCreatableName(newPagePath)) {
+      return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath})'`, 'invalid_path'), 409);
+    }
+
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
+    const isExist = await Page.count({ path: newPagePath }) > 0;
+    if (isExist) {
+      // if page found, cannot cannot rename to that path
+      return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
+    }
+
+    let page;
+
+    try {
+      page = await Page.findByIdAndViewer(pageId, req.user);
+
+      if (page == null) {
+        return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
+      }
+
+      if (!page.isUpdatable(revisionId)) {
+        return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
+      }
+
+      if (isRecursively) {
+        page = await Page.renameRecursively(page, newPagePath, req.user, options);
+      }
+      else {
+        page = await Page.rename(page, newPagePath, req.user, options);
+      }
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
+    }
+
+    const result = { page: pageService.serializeToObj(page) };
+
+    try {
+      // global notification
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
+        oldPath: req.body.path,
+      });
+    }
+    catch (err) {
+      logger.error('Move notification failed', err);
     }
     }
+
+    return res.apiv3(result);
   });
   });
 
 
+
   /**
   /**
   * @swagger
   * @swagger
   *
   *
@@ -172,9 +356,7 @@ module.exports = (crowi) => {
       return res.apiv3({ pages });
       return res.apiv3({ pages });
     }
     }
     catch (err) {
     catch (err) {
-      res.code = 'unknown';
-      logger.error('Failed to delete trash pages', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
     }
     }
   });
   });
 
 
@@ -238,9 +420,7 @@ module.exports = (crowi) => {
       return res.apiv3({ resultPaths });
       return res.apiv3({ resultPaths });
     }
     }
     catch (err) {
     catch (err) {
-      res.code = 'unknown';
-      logger.error('Failed to find the path', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
     }
     }
 
 
   });
   });

+ 0 - 1
src/server/routes/index.js

@@ -138,7 +138,6 @@ module.exports = function(crowi, app) {
   app.get('/_api/users.list'          , accessTokenParser , loginRequired , user.api.list);
   app.get('/_api/users.list'          , accessTokenParser , loginRequired , user.api.list);
   app.get('/_api/pages.list'          , accessTokenParser , loginRequired , page.api.list);
   app.get('/_api/pages.list'          , accessTokenParser , loginRequired , page.api.list);
   app.get('/_api/pages.recentCreated' , accessTokenParser , loginRequired , page.api.recentCreated);
   app.get('/_api/pages.recentCreated' , accessTokenParser , loginRequired , page.api.recentCreated);
-  app.post('/_api/pages.create'       , accessTokenParser , loginRequiredStrictly , csrf, page.api.create);
   app.post('/_api/pages.update'       , accessTokenParser , loginRequiredStrictly , csrf, page.api.update);
   app.post('/_api/pages.update'       , accessTokenParser , loginRequiredStrictly , csrf, page.api.update);
   app.get('/_api/pages.get'           , accessTokenParser , loginRequired , page.api.get);
   app.get('/_api/pages.get'           , accessTokenParser , loginRequired , page.api.get);
   app.get('/_api/pages.exist'         , accessTokenParser , loginRequired , page.api.exist);
   app.get('/_api/pages.exist'         , accessTokenParser , loginRequired , page.api.exist);

+ 1 - 49
src/server/routes/page.js

@@ -676,55 +676,6 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
-  /**
-   * @swagger
-   *
-   *    /pages.create:
-   *      post:
-   *        tags: [Pages, CrowiCompatibles]
-   *        operationId: createPage
-   *        summary: /pages.create
-   *        description: Create page
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  body:
-   *                    $ref: '#/components/schemas/Revision/properties/body'
-   *                  path:
-   *                    $ref: '#/components/schemas/Page/properties/path'
-   *                  grant:
-   *                    $ref: '#/components/schemas/Page/properties/grant'
-   *                required:
-   *                  - body
-   *                  - path
-   *        responses:
-   *          200:
-   *            description: Succeeded to create page.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /pages.create Create new page
-   * @apiName CreatePage
-   * @apiGroup Page
-   *
-   * @apiParam {String} body
-   * @apiParam {String} path
-   * @apiParam {String} grant
-   * @apiParam {Array} pageTags
-   */
   // TODO If everything that depends on this route, delete it too
   // TODO If everything that depends on this route, delete it too
   api.create = async function(req, res) {
   api.create = async function(req, res) {
     const body = req.body.body || null;
     const body = req.body.body || null;
@@ -1401,6 +1352,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} new_path New path name.
    * @apiParam {String} new_path New path name.
    * @apiParam {Bool} create_redirect
    * @apiParam {Bool} create_redirect
    */
    */
+  // TODO remove after GW-3429 and GW-3430
   api.rename = async function(req, res) {
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
     const previousRevision = req.body.revision_id || null;

+ 1 - 1
src/server/views/admin/customize.html

@@ -7,7 +7,7 @@
 {{ cdnStyleTag('jquery-ui') }}
 {{ cdnStyleTag('jquery-ui') }}
 <style>
 <style>
   .CodeMirror {
   .CodeMirror {
-    border: 1px solid #eee;
+    border: 1px solid $gray-100;
   }
   }
 </style>
 </style>
 {% endblock %}
 {% endblock %}

+ 83 - 0
src/test/middlewares/access-token-parser.test.js

@@ -0,0 +1,83 @@
+const mongoose = require('mongoose');
+
+const { getInstance } = require('../setup-crowi');
+
+describe('accessTokenParser', () => {
+  let crowi;
+  let accessTokenParser;
+
+  let User;
+  let targetUser;
+
+  beforeAll(async(done) => {
+    crowi = await getInstance();
+    User = mongoose.model('User');
+    accessTokenParser = require('@server/middlewares/access-token-parser')(crowi);
+
+    targetUser = await User.create({
+      name: 'Example for access token parser',
+      username: 'targetUser',
+      password: 'usertestpass',
+      lang: 'en_US',
+      apiToken: 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=',
+    });
+
+
+    done();
+  });
+
+  crowi = {
+    model: jest.fn().mockReturnValue(User),
+  };
+  const req = {
+    skipCsrfVerify: false,
+    query: {},
+    body: {},
+    user: {},
+  };
+
+  const res = {};
+  const next = jest.fn().mockReturnValue('next');
+
+  test('without accessToken', async() => {
+    const result = await accessTokenParser(req, res, next);
+
+    expect(next).toHaveBeenCalled();
+    expect(result).toBe('next');
+    expect(req.skipCsrfVerify).toBe(false);
+  });
+
+  test('with invalid accessToken', async() => {
+    req.query.access_token = 'invalidAccessToken';
+
+    const result = await accessTokenParser(req, res, next);
+
+    expect(next).toHaveBeenCalled();
+    expect(result).toBe('next');
+    expect(req.skipCsrfVerify).toBe(false);
+  });
+
+  test('with accessToken in query', async() => {
+    req.query.access_token = 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=';
+
+    const result = await accessTokenParser(req, res, next);
+
+    expect(next).toHaveBeenCalled();
+    expect(result).toBe('next');
+    expect(req.skipCsrfVerify).toBe(true);
+    expect(req.user._id).toStrictEqual(targetUser._id);
+  });
+
+  test('with accessToken in body', async() => {
+    req.body.access_token = 'N4xPDjh48TBsC7ahUN+ajjL5asnGpwtA5VAR+EhIDeg=';
+
+    const result = await accessTokenParser(req, res, next);
+
+    expect(next).toHaveBeenCalled();
+    expect(result).toBe('next');
+    expect(req.skipCsrfVerify).toBe(true);
+    expect(req.user._id).toStrictEqual(targetUser._id);
+  });
+
+
+});