Răsfoiți Sursa

Merge pull request #1760 from weseek/support/reactify-grw-subnav

create component
itizawa 6 ani în urmă
părinte
comite
52311eaded

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

+ 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,

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

+ 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"