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

Merge branch 'support/apply-bootstrap4' into support/bstr4-group-setting-page

# Conflicts:
#	src/client/styles/scss/_override-bootstrap.scss
yusuketk 6 лет назад
Родитель
Сommit
7306f40940

+ 4 - 0
resource/locales/en-US/translation.json

@@ -565,6 +565,10 @@
     "channel": "Channel",
     "channel": "Channel",
     "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
     "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
     "channel_desc": "Slack channel name. Without <code>#</code>.",
     "channel_desc": "Slack channel name. Without <code>#</code>.",
+    "valid_page": "Enable/Disable Notification",
+    "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
+    "just_me_notification_help": "<strong>The page that is restricted by 'only me'</strong> is notify when the page edited.",
+    "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
     "notification_list": "List of Notification Settings",
     "notification_list": "List of Notification Settings",
     "add_notification": "Add New",
     "add_notification": "Add New",
     "trigger_path": "Trigger Path",
     "trigger_path": "Trigger Path",

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

@@ -35,6 +35,7 @@ import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import TagContainer from './services/TagContainer';
+import GrowiSubNavigation from './components/Navbar/GrowiSubNavigation';
 
 
 import { appContainer, componentMappings } from './bootstrap';
 import { appContainer, componentMappings } from './bootstrap';
 
 
@@ -100,6 +101,7 @@ if (pageContainer.state.path != null) {
     'page': <Page />,
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label': <TagLabels />,
     'tag-label': <TagLabels />,
+    'grw-subnav': <GrowiSubNavigation />,
   });
   });
 }
 }
 
 

+ 10 - 9
src/client/js/components/Admin/Notification/GlobalNotification.jsx

@@ -42,25 +42,25 @@ class GlobalNotification extends React.Component {
 
 
         <h2 className="border-bottom">{t('notification_setting.valid_page')}</h2>
         <h2 className="border-bottom">{t('notification_setting.valid_page')}</h2>
 
 
-        <p className="well">
+        <p className="card well">
           {/* eslint-disable-next-line react/no-danger */}
           {/* eslint-disable-next-line react/no-danger */}
           <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
           <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
         </p>
         </p>
 
 
 
 
         <div className="row mb-4">
         <div className="row mb-4">
-          <div className="col-md-8 col-md-offset-2">
-            <div className="checkbox checkbox-success">
+          <div className="col-md-8 offset-md-2">
+            <div className="custom-control custom-checkbox custom-checkbox-success">
               <input
               <input
                 id="isNotificationForOwnerPageEnabled"
                 id="isNotificationForOwnerPageEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
                 checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
               />
               />
-              <label htmlFor="isNotificationForOwnerPageEnabled">
+              <label className="custom-control-label" htmlFor="isNotificationForOwnerPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
-
               </label>
               </label>
             </div>
             </div>
           </div>
           </div>
@@ -68,15 +68,16 @@ class GlobalNotification extends React.Component {
 
 
 
 
         <div className="row mb-4">
         <div className="row mb-4">
-          <div className="col-md-8 col-md-offset-2">
-            <div className="checkbox checkbox-success">
+          <div className="col-md-8 offset-md-2">
+            <div className="custom-control custom-checkbox custom-checkbox-success">
               <input
               <input
                 id="isNotificationForGroupPageEnabled"
                 id="isNotificationForGroupPageEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
                 checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
               />
               />
-              <label htmlFor="isNotificationForGroupPageEnabled">
+              <label className="custom-control-label" htmlFor="isNotificationForGroupPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
               </label>
               </label>
@@ -85,7 +86,7 @@ class GlobalNotification extends React.Component {
         </div>
         </div>
 
 
         <div className="row my-3">
         <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
+          <div className="col-sm-5 offset-sm-4">
             <button
             <button
               type="button"
               type="button"
               className="btn btn-primary"
               className="btn btn-primary"

+ 81 - 0
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -0,0 +1,81 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { isTrashPage } from '../../../../lib/util/path-utils';
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import RevisionPath from '../Page/RevisionPath';
+import PageContainer from '../../services/PageContainer';
+import TagLabels from '../Page/TagLabels';
+import LikeButton from '../LikeButton';
+import BookmarkButton from '../BookmarkButton';
+import PageCreator from './PageCreator';
+import RevisionAuthor from './RevisionAuthor';
+
+const GrowiSubNavigation = (props) => {
+  const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page');
+  const { appContainer, pageContainer } = props;
+  const {
+    path, createdAt, creator, updatedAt, revisionAuthor,
+  } = pageContainer.state;
+
+  // Display only the RevisionPath if the page is trash or forbidden
+  if (isTrashPage(path) || isPageForbidden) {
+    return (
+      <div className="d-flex align-items-center">
+        <div className="title-container mr-auto">
+          <h1>
+            <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />
+          </h1>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="d-flex align-items-center">
+
+      {/* Page Path */}
+      <div className="title-container mr-auto">
+        <h1>
+          <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />
+        </h1>
+        <TagLabels />
+      </div>
+
+      {/* Header Button */}
+      <div className="ml-1">
+        <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />
+      </div>
+      <div>
+        <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />
+      </div>
+
+      {/* Page Authors */}
+      <ul className="authors hidden-sm hidden-xs text-nowrap">
+        {creator != null && <li><PageCreator creator={creator} createdAt={createdAt} /></li>}
+        {revisionAuthor != null && <li className="mt-1"><RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} /></li>}
+      </ul>
+
+    </div>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GrowiSubNavigationWrapper = (props) => {
+  return createSubscribedElement(GrowiSubNavigation, props, [AppContainer, PageContainer]);
+};
+
+
+GrowiSubNavigation.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(GrowiSubNavigationWrapper);

+ 30 - 0
src/client/js/components/Navbar/PageCreator.jsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import UserPicture from '../User/UserPicture';
+import { userPageRoot } from '../../../../lib/util/path-utils';
+
+const PageCreator = (props) => {
+  const { creator, createdAt } = props;
+
+  return (
+    <div className="d-flex align-items-center">
+      <div className="mr-2" href={userPageRoot(creator)} data-toggle="tooltip" data-placement="bottom" title={creator.name}>
+        <UserPicture user={creator} size="sm" />
+      </div>
+      <div>
+        <div>Created by <a href={userPageRoot(creator)}>{creator.name}</a></div>
+        <div className="text-muted">{createdAt}</div>
+      </div>
+    </div>
+  );
+};
+
+PageCreator.propTypes = {
+
+  creator: PropTypes.object.isRequired,
+  createdAt: PropTypes.string.isRequired,
+};
+
+
+export default PageCreator;

+ 30 - 0
src/client/js/components/Navbar/RevisionAuthor.jsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import UserPicture from '../User/UserPicture';
+import { userPageRoot } from '../../../../lib/util/path-utils';
+
+const RevisionAuthor = (props) => {
+  const { revisionAuthor, updatedAt } = props;
+
+  return (
+    <div className="d-flex align-items-center">
+      <div className="mr-2" href={userPageRoot(revisionAuthor)} data-toggle="tooltip" data-placement="bottom" title={revisionAuthor.name}>
+        <UserPicture user={revisionAuthor} size="sm" />
+      </div>
+      <div>
+        <div>Updated by  <a href={userPageRoot(revisionAuthor)}>{revisionAuthor.name}</a></div>
+        <div className="text-muted">{updatedAt}</div>
+      </div>
+    </div>
+  );
+};
+
+RevisionAuthor.propTypes = {
+
+  revisionAuthor: PropTypes.object.isRequired,
+  updatedAt: PropTypes.string.isRequired,
+};
+
+
+export default RevisionAuthor;

+ 1 - 1
src/client/js/components/Page/CopyDropdown.jsx

@@ -59,7 +59,7 @@ export default class CopyDropdown extends React.Component {
 
 
           <DropdownToggle
           <DropdownToggle
             caret
             caret
-            className="d-block text-muted bg-transparent btn-copy"
+            className="d-block text-muted bg-transparent btn-copy border-0"
             style={this.props.buttonStyle}
             style={this.props.buttonStyle}
           >
           >
             <i className="ti-clipboard"></i>
             <i className="ti-clipboard"></i>

+ 4 - 0
src/client/js/services/PageContainer.js

@@ -35,11 +35,15 @@ export default class PageContainer extends Container {
       pageId: mainContent.getAttribute('data-page-id'),
       pageId: mainContent.getAttribute('data-page-id'),
       revisionId,
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
+      revisionAuthor: JSON.parse(mainContent.getAttribute('data-page-revision-author')),
       path: mainContent.getAttribute('data-path'),
       path: mainContent.getAttribute('data-path'),
       tocHtml: '',
       tocHtml: '',
       isLiked: false,
       isLiked: false,
       seenUserIds: [],
       seenUserIds: [],
       likerUserIds: [],
       likerUserIds: [],
+      createdAt: mainContent.getAttribute('data-page-created-at'),
+      creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
+      updatedAt: mainContent.getAttribute('data-page-updated-at'),
 
 
       tags: [],
       tags: [],
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       templateTagData: mainContent.getAttribute('data-template-tags') || null,

+ 9 - 4
src/client/styles/scss/_create-page.scss

@@ -1,12 +1,17 @@
 .modal.create-page {
 .modal.create-page {
   // more than tablet size
   // more than tablet size
-  @include media-breakpoint-up(sm) {
+  @include media-breakpoint-down(sm) {
     .modal-dialog {
     .modal-dialog {
-      width: 45%;
-      max-width: initial;
+      max-width: 100%;
+      margin: 10px;
     }
     }
   }
   }
 
 
+  .modal-dialog {
+    max-width: 750px;
+    box-sizing: border-box;
+  }
+
   .modal-body {
   .modal-body {
     //TODO remove legend
     //TODO remove legend
     legend {
     legend {
@@ -28,7 +33,7 @@
         }
         }
 
 
         // change layout by screen size
         // change layout by screen size
-        @include media-breakpoint-down(xs) {
+        @include media-breakpoint-down(sm) {
           flex-direction: column;
           flex-direction: column;
           .create-page-button-container {
           .create-page-button-container {
             margin-top: 10px;
             margin-top: 10px;

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

@@ -162,9 +162,10 @@ fieldset[disabled] .btn {
 }
 }
 
 
 // input form (disabled box-shadow added in bootstrap4)
 // input form (disabled box-shadow added in bootstrap4)
-html div.form-group .form-control {
+form.form-group input.form-control {
   &:focus,
   &:focus,
   &.focus {
   &.focus {
+    border-color: inherit;
     box-shadow: none;
     box-shadow: none;
   }
   }
 }
 }

+ 30 - 0
src/lib/util/path-utils.js

@@ -0,0 +1,30 @@
+
+/**
+ * Whether path belongs to the trash page
+ * @param {string} path
+ * @returns {boolean}
+ */
+const isTrashPage = (path) => {
+  if (path.match(/^\/trash(\/.*)?$/)) {
+    return true;
+  }
+
+  return false;
+};
+
+/**
+ * return user path
+ * @param {Object} user
+ * @return {string}
+ */
+const userPageRoot = (user) => {
+  if (!user || !user.username) {
+    return '';
+  }
+  return `/user/${user.username}`;
+};
+
+module.exports = {
+  isTrashPage,
+  userPageRoot,
+};

+ 1 - 37
src/server/views/layout-growi/widget/header.html

@@ -1,47 +1,11 @@
 <header id="page-header">
 <header id="page-header">
 
 
-  <div class="d-flex align-items-center">
-
-    <div class="title-container mr-auto">
-      <h1 class="title" id="revision-path"></h1>
-      {% if not forbidden and not isTrashPage() %}
-        <div id="tag-label"></div>
-      {% endif %}
-    </div><!-- /.title-container -->
-    {% if page %}
-    {% include '../../widget/header-buttons.html' %}
-
-    <ul class="authors hidden-sm hidden-xs text-nowrap">
-      <li>
-        <div class="d-flex align-items-center">
-          <a class="mr-2" href="{{ userPageRoot(page.creator) }}" data-toggle="tooltip" data-placement="bottom" title="{{ page.creator.name|default(author.name) }}">
-            <img src="{{ page.creator|default(author)|picture }}" class="picture rounded-circle">
-          </a>
-          <div>
-            <div>Created by <a href="{{ userPageRoot(page.creator) }}">{{ page.creator.name|default(author.name) }}</a></div>
-            <div class="text-muted">{{ page.createdAt|datetz('Y/m/d H:i:s') }}</div>
-          </div>
-        </div>
-      </li>
-      <li class="mt-1">
-        <div class="d-flex align-items-center">
-          <a class="mr-2" href="{{ userPageRoot(author) }}" data-toggle="tooltip" data-placement="bottom" title="{{ author.name }}">
-            <img src="{{ author|picture }}" class="picture rounded-circle">
-          </a>
-          <div>
-            <div>Updated by <a href="{{ userPageRoot(page.revision.author) }}">{{ author.name }}</a></div>
-            <div class="text-muted"">{{ page.updatedAt|datetz('Y/m/d H:i:s') }}</div>
-          </div>
-        </div>
-      </li>
-    </ul><!-- /.authors -->
-    {% endif %}
+  <div id="grw-subnav" data-is-forbidden-page="{{ forbidden }}"></div>
 
 
     {% if not page and not forbidden and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
     {% if not page and not forbidden and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
       {% if '/' === path.slice(-1) %}
       {% if '/' === path.slice(-1) %}
         {% include '../../widget/create_portal.html' %}
         {% include '../../widget/create_portal.html' %}
       {% endif %}
       {% endif %}
     {% endif %}
     {% endif %}
-  </div>
 
 
 </header>
 </header>

+ 3 - 3
src/server/views/modal/create_page.html

@@ -9,7 +9,7 @@
 
 
       <div class="modal-body">
       <div class="modal-body">
 
 
-        <form class="row form-horizontal" id="create-page-today" role="form">
+        <form class="row form-group" id="create-page-today" role="form">
           <fieldset class="col-12 mb-4">
           <fieldset class="col-12 mb-4">
             <h3 class="grw-modal-head pb-2">{{ t("Create today's") }}</h3>
             <h3 class="grw-modal-head pb-2">{{ t("Create today's") }}</h3>
             <div class="d-flex create-page-input-container">
             <div class="d-flex create-page-input-container">
@@ -26,7 +26,7 @@
           </fieldset>
           </fieldset>
         </form>
         </form>
 
 
-        <form class="row form-horizontal" id="create-page-under-tree" role="form">
+        <form class="row form-group" id="create-page-under-tree" role="form">
           <fieldset class="col-12 mb-4">
           <fieldset class="col-12 mb-4">
             <h3 class="grw-modal-head pb-2">{{ t('Create under') }}</h3>
             <h3 class="grw-modal-head pb-2">{{ t('Create under') }}</h3>
             <div class="d-flex create-page-input-container">
             <div class="d-flex create-page-input-container">
@@ -45,7 +45,7 @@
         </form>
         </form>
 
 
         {% set templateParentPath = parentPath(path | preventXss | escape) %}
         {% set templateParentPath = parentPath(path | preventXss | escape) %}
-        <div id="template-form" class="row form-horizontal">
+        <div id="template-form" class="row form-group">
           <fieldset class="col-12">
           <fieldset class="col-12">
             <h3 class="grw-modal-head pb-2">{{ t('template.modal_label.Create template under', templateParentPath) }}</h3>
             <h3 class="grw-modal-head pb-2">{{ t('template.modal_label.Create template under', templateParentPath) }}</h3>
             <div class="d-flex create-page-input-container">
             <div class="d-flex create-page-input-container">

+ 4 - 0
src/server/views/widget/page_content.html

@@ -5,11 +5,15 @@
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
+  data-page-revision-author="{% if revision %}{{ revision.author|json }}{% endif %}"
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
+  data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-creator="{% if page %}{{ page.creator|json }}{% endif %}"
+  data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   >
   >
 {% else %}
 {% else %}
 <div id="content-main" class="content-main"
 <div id="content-main" class="content-main"