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

Merge pull request #1970 from weseek/fix/grw-subnav

Fix/grw subnav
Yuki Takei 6 лет назад
Родитель
Сommit
cb7a503b66
49 измененных файлов с 419 добавлено и 1083 удалено
  1. 50 30
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  2. 37 46
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  3. 16 12
      src/client/js/components/Page/RevisionPath.jsx
  4. 1 1
      src/client/js/components/Page/TagEditor.jsx
  5. 19 7
      src/client/js/services/PageContainer.js
  6. 0 7
      src/client/styles/agile-admin/inverse/colors/_apply-colors-dark.scss
  7. 0 4
      src/client/styles/agile-admin/inverse/colors/antarctic.scss
  8. 0 8
      src/client/styles/agile-admin/inverse/colors/spring.scss
  9. 4 0
      src/client/styles/scss/_admin.scss
  10. 0 25
      src/client/styles/scss/_layout.scss
  11. 1 1
      src/client/styles/scss/_layout_kibela.scss
  12. 5 0
      src/client/styles/scss/_me.scss
  13. 3 3
      src/client/styles/scss/_on-edit.scss
  14. 0 41
      src/client/styles/scss/_page_growi.scss
  15. 0 10
      src/client/styles/scss/_page_header.scss
  16. 97 0
      src/client/styles/scss/_subnav.scss
  17. 21 17
      src/client/styles/scss/_tag.scss
  18. 52 27
      src/client/styles/scss/_user.scss
  19. 0 7
      src/client/styles/scss/_user_growi.scss
  20. 2 2
      src/client/styles/scss/style-app.scss
  21. 18 19
      src/client/styles/scss/theme/_apply-colors-dark.scss
  22. 15 12
      src/client/styles/scss/theme/_apply-colors-light.scss
  23. 0 11
      src/client/styles/scss/theme/_apply-colors.scss
  24. 1 3
      src/server/views/admin/Users_reserve.html
  25. 2 3
      src/server/views/admin/app.html
  26. 1 3
      src/server/views/admin/customize.html
  27. 1 3
      src/server/views/admin/export.html
  28. 1 3
      src/server/views/admin/external-accounts.html
  29. 1 3
      src/server/views/admin/global-notification-detail.html
  30. 1 3
      src/server/views/admin/importer.html
  31. 1 3
      src/server/views/admin/index.html
  32. 1 3
      src/server/views/admin/markdown.html
  33. 1 3
      src/server/views/admin/notification.html
  34. 1 3
      src/server/views/admin/search.html
  35. 1 3
      src/server/views/admin/security.html
  36. 1 3
      src/server/views/admin/user-group-detail.html
  37. 1 3
      src/server/views/admin/user-groups.html
  38. 1 3
      src/server/views/admin/users.html
  39. 1 1
      src/server/views/layout-crowi/base/layout.html
  40. 6 7
      src/server/views/layout-growi/base/layout.html
  41. 8 6
      src/server/views/layout-growi/user_page.html
  42. 6 10
      src/server/views/layout-growi/widget/header.html
  43. 1 1
      src/server/views/layout-kibela/base/layout.html
  44. 1 1
      src/server/views/layout-kibela/user_page.html
  45. 0 85
      src/server/views/me/api_token.html
  46. 0 249
      src/server/views/me/external-accounts.html
  47. 4 354
      src/server/views/me/index.html
  48. 8 9
      src/server/views/tags.html
  49. 27 25
      src/server/views/widget/page_tabs.html

+ 50 - 30
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import { isTrashPage } from '../../../../lib/util/path-utils';
+import { isTrashPage } from '@commons/util/path-utils';
+
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import RevisionPath from '../Page/RevisionPath';
@@ -18,53 +19,72 @@ const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page');
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isCompactMode,
+    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isHeaderSticky, isSubnavCompact,
   } = pageContainer.state;
-  const compactClassName = isCompactMode ? 'grw-compact-subnavbar' : null;
 
-  // Display only the RevisionPath if the page is trash or forbidden
-  if (isTrashPage(path) || isPageForbidden) {
+  const isPageNotFound = pageId == null;
+  const isPageInTrash = isTrashPage(path);
+
+  // Display only the RevisionPath
+  if (isPageNotFound || isPageForbidden || isPageInTrash) {
     return (
-      <div className="d-flex align-items-center">
-        <div className="title-container mr-auto">
-          <h1>
-            <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
-          </h1>
-        </div>
+      <div className="d-flex align-items-center px-3 py-3 grw-subnavbar">
+        <h1 className="m-0">
+          <RevisionPath
+            behaviorType={appContainer.config.behaviorType}
+            pageId={pageId}
+            pagePath={pageContainer.state.path}
+            isPageNotFound
+            isPageForbidden
+            isPageInTrash
+          />
+        </h1>
       </div>
     );
   }
 
+  const additionalClassNames = ['grw-subnavbar'];
+  if (isHeaderSticky) {
+    additionalClassNames.push('grw-subnavbar-sticky');
+  }
+  if (isSubnavCompact) {
+    additionalClassNames.push('grw-subnavbar-compact');
+  }
+
   return (
-    <div className={`d-flex px-3 py-1 align-items-center ${compactClassName}`}>
+    <div className={`d-flex align-items-center justify-content-between px-3 py-1 ${additionalClassNames.join(' ')}`}>
 
       {/* Page Path */}
-      <div className="title-container mr-auto">
-        <h1>
+      <div>
+        <h1 className="m-0">
           <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
         </h1>
         <TagLabels />
       </div>
 
-      {/* Header Button */}
-      <div className="mr-2">
-        <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
-      </div>
-      <div>
-        <BookmarkButton pageId={pageId} crowi={appContainer} />
-      </div>
+      <div className="d-flex align-items-center">
+        {/* Header Button */}
+        <div className="mr-2">
+          <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
+        </div>
+        <div>
+          <BookmarkButton pageId={pageId} crowi={appContainer} />
+        </div>
 
-      {/* Page Authors */}
-      <ul className="authors text-nowrap d-none d-lg-block">
-        {creator != null && <li><PageCreator creator={creator} createdAt={createdAt} isCompactMode={isCompactMode} /></li>}
-        { revisionAuthor != null
-          && (
+        {/* Page Authors */}
+        <ul className="authors text-nowrap d-none d-lg-block">
+          { creator != null && (
+            <li>
+              <PageCreator creator={creator} createdAt={createdAt} isCompactMode={isSubnavCompact} />
+            </li>
+          ) }
+          { revisionAuthor != null && (
             <li className="mt-1">
-              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isCompactMode} />
+              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isSubnavCompact} />
             </li>
-          )
-        }
-      </ul>
+          ) }
+        </ul>
+      </div>
 
     </div>
   );

+ 37 - 46
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -1,8 +1,7 @@
-import React, { useState, useEffect } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
-import { throttle } from 'throttle-debounce';
 
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
@@ -14,55 +13,47 @@ import UserPicture from '../User/UserPicture';
 const GrowiSubNavigationForUserPage = (props) => {
   const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
   const { appContainer, pageContainer } = props;
-  const { pageId } = pageContainer.state;
-  const [isCompactMode, setIsCompactMode] = useState(false);
-  const scrollAmountForFixed = 50;
-  const layoutType = appContainer.getConfig().layoutType;
-
-  useEffect(() => {
-    window.addEventListener('scroll', throttle(300, () => {
-      setIsCompactMode(window.pageYOffset > scrollAmountForFixed);
-    }));
-  }, []);
+  const { pageId, isHeaderSticky, isSubnavCompact } = pageContainer.state;
+
+  const additionalClassNames = ['grw-subnavbar', 'grw-subnavbar-user-page'];
+  if (isHeaderSticky) {
+    additionalClassNames.push('grw-subnavbar-sticky');
+  }
+  if (isSubnavCompact) {
+    additionalClassNames.push('py-2 grw-subnavbar-compact');
+  }
+  else {
+    additionalClassNames.push('py-3');
+  }
 
   return (
-    <div className={`row px-3 py-1 ${(isCompactMode && layoutType === 'growi') && 'grw-compact-subnavbar'}`}>
-
-      <div className="col-12">
-        {/* Page Path */}
-        <h4>
-          <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
-        </h4>
-
-        <div className="d-flex">
-          <div className="users-info d-flex align-items-center mr-auto">
-            <UserPicture user={pageUser} />
-
-            <div className="users-meta">
-              <div className="d-flex align-items-center">
-                <h1>
-                  {pageUser.name}
-                </h1>
-              </div>
-              <div className="user-page-meta">
-                <ul>
-                  <li className="user-page-username"><i className="icon-user mr-1"></i>{pageUser.username}</li>
-                  <li className="user-page-email">
-                    <i className="icon-envelope mr-1"></i>
-                    {pageUser.isEmailPublished ? pageUser.email : '*****'}
-                  </li>
-                  {pageUser.introduction && <li className="user-page-introduction"><p>{pageUser.introduction}</p></li>}
-                </ul>
-              </div>
-            </div>
+    <div className={`px-3 ${additionalClassNames.join(' ')}`}>
+      <h4 className="grw-user-page-path">
+        <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
+      </h4>
+
+      <div className="d-flex align-items-center justify-content-between">
+
+        <div className="users-info d-flex align-items-center">
+          <UserPicture user={pageUser} />
+
+          <div className="users-meta">
+            <h1>
+              {pageUser.name}
+            </h1>
+            <ul className="user-page-meta mt-1 mb-0">
+              <li className="user-page-username"><i className="icon-user mr-1"></i>{pageUser.username}</li>
+              <li className="user-page-email">
+                <i className="icon-envelope mr-1"></i>
+                {pageUser.isEmailPublished ? pageUser.email : '*****'}
+              </li>
+              {pageUser.introduction && <li className="user-page-introduction"><p>{pageUser.introduction}</p></li>}
+            </ul>
           </div>
-
-          {/* Header Button */}
-          <BookmarkButton pageId={pageId} crowi={appContainer} size="lg" />
         </div>
-      </div>
-
 
+        <BookmarkButton pageId={pageId} crowi={appContainer} size="lg" />
+      </div>
     </div>
   );
 

+ 16 - 12
src/client/js/components/Page/RevisionPath.jsx

@@ -16,7 +16,6 @@ class RevisionPath extends React.Component {
       pages: [],
       isListPage: false,
       isLinkToListPage: true,
-      isInTrash: false,
     };
 
     // retrieve xss library from window
@@ -60,12 +59,6 @@ class RevisionPath extends React.Component {
     const pages = [];
     const pagePaths = [];
     splitted.forEach((pageName) => {
-      // skip trash
-      if (pageName === 'trash' && splitted.length > 1) {
-        this.setState({ isInTrash: true });
-        return;
-      }
-
       pagePaths.push(encodeURIComponent(pageName));
       pages.push({
         pagePath: urljoin('/', ...pagePaths),
@@ -109,10 +102,10 @@ class RevisionPath extends React.Component {
       padding: '0 2px',
     };
 
-    const { isInTrash } = this.state;
+    const { isPageInTrash, isPageForbidden } = this.props;
     const pageLength = this.state.pages.length;
 
-    const rootElement = isInTrash
+    const rootElement = isPageInTrash
       ? (
         <>
           <span className="path-segment">
@@ -159,9 +152,11 @@ class RevisionPath extends React.Component {
 
         <CopyDropdown t={this.props.t} pagePath={this.props.pagePath} pageId={this.props.pageId} buttonStyle={buttonStyle}></CopyDropdown>
 
-        <a href="#edit" className="d-block text-muted btn btn-secondary bg-transparent btn-edit border-0" style={buttonStyle}>
-          <i className="icon-note" />
-        </a>
+        { !isPageInTrash && !isPageForbidden && (
+          <a href="#edit" className="d-block text-muted btn btn-secondary bg-transparent btn-edit border-0" style={buttonStyle}>
+            <i className="icon-note" />
+          </a>
+        ) }
       </span>
     );
   }
@@ -173,6 +168,15 @@ RevisionPath.propTypes = {
   behaviorType: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   pageId: PropTypes.string,
+  isPageNotFound: PropTypes.bool,
+  isPageForbidden: PropTypes.bool,
+  isPageInTrash: PropTypes.bool,
+};
+
+RevisionPath.defaultProps = {
+  isPageNotFound: false,
+  isPageForbidden: false,
+  isPageInTrash: false,
 };
 
 export default withTranslation()(RevisionPath);

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

@@ -46,7 +46,7 @@ export default class TagEditor extends React.Component {
 
   render() {
     return (
-      <Modal isOpen={this.state.isOpenModal} toggle={this.closeModalHandler} id="editTagModal">
+      <Modal isOpen={this.state.isOpenModal} toggle={this.closeModalHandler} id="edit-tag-modal">
         <ModalHeader tag="h4" toggle={this.closeModalHandler} className="bg-primary">
           <span className="text-white">Edit Tags</span>
         </ModalHeader>

+ 19 - 7
src/client/js/services/PageContainer.js

@@ -5,10 +5,10 @@ import loggerFactory from '@alias/logger';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
 
-import { throttle } from 'throttle-debounce';
-
 const logger = loggerFactory('growi:services:PageContainer');
-const scrollAmountForFixed = 50;
+const scrollThresForSticky = 50;
+const scrollThresForCompact = 100;
+const scrollThresForThrottling = 200;
 
 /**
  * Service container related to Page
@@ -58,7 +58,9 @@ export default class PageContainer extends Container {
       pageIdOnHackmd: mainContent.getAttribute('data-page-id-on-hackmd') || null,
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       isHackmdDraftUpdatingInRealtime: false,
-      isCompactMode: false,
+
+      isHeaderSticky: false,
+      isSubnavCompact: false,
     };
 
     this.initStateMarkdown();
@@ -69,9 +71,19 @@ export default class PageContainer extends Container {
     this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
     this.addWebSocketEventHandlers();
 
-    window.addEventListener('scroll', throttle(300, () => {
-      this.setState({ isCompactMode: window.pageYOffset > scrollAmountForFixed });
-    }));
+    window.addEventListener('scroll', () => {
+      const currentYOffset = window.pageYOffset;
+
+      // original throttling
+      if (this.state.isSubnavCompact && scrollThresForThrottling < currentYOffset) {
+        return;
+      }
+
+      this.setState({
+        isHeaderSticky: scrollThresForSticky < currentYOffset,
+        isSubnavCompact: scrollThresForCompact < currentYOffset,
+      });
+    });
   }
 
   /**

+ 0 - 7
src/client/styles/agile-admin/inverse/colors/_apply-colors-dark.scss

@@ -1,10 +1,3 @@
-.top-left-part {
-  .logo-mark,
-  .logo-text {
-    fill: white;
-  }
-}
-
 /*
  * Button
  */

+ 0 - 4
src/client/styles/agile-admin/inverse/colors/antarctic.scss

@@ -91,10 +91,6 @@ table,
  * Accentcolor (yellow)
  */
 
-header.affix {
-  border-bottom: 4px solid $accentcolor;
-}
-
 .modal {
   .modal-header {
     border-bottom: 4px solid $accentcolor;

+ 0 - 8
src/client/styles/agile-admin/inverse/colors/spring.scss

@@ -49,14 +49,6 @@ $wikilinktext-hover: gba(171, 224, 174, 0.9);
   border-top-color: $third-main-color;
 }
 
-/*
- * Accentcolor (green)
- */
-
-header.affix {
-  border-bottom: 4px solid $accentcolor;
-}
-
 .modal {
   .modal-header {
     border-bottom: 4px solid $accentcolor;

+ 4 - 0
src/client/styles/scss/_admin.scss

@@ -1,4 +1,8 @@
 .admin-page {
+  .grw-header.sticky-top {
+    height: unset;
+  }
+
   .admin-user-menu {
     .dropdown-menu {
       right: 0;

+ 0 - 25
src/client/styles/scss/_layout.scss

@@ -47,36 +47,11 @@
 .grw-sidebar-content-container {
 }
 
-/*
-  * header
-  */
-.grw-subnav {
-  overflow: unset;
-}
-
 .grw-modal-head {
   font-size: 1em;
   border-bottom: 1px solid $grw-line-gray;
 }
 
-header#page-header {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
-
-  line-height: 1em;
-  // the container of h1
-  div.title-container {
-    padding-right: 5px;
-    padding-left: 5px;
-    margin-right: auto;
-  }
-
-  h1 {
-    @include variable-font-size(28px);
-    line-height: 1.1em;
-  }
-}
-
 .main {
   margin-top: 1rem;
 }

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

@@ -221,7 +221,7 @@ body.kibela {
 
     .card-footer {
       background: white;
-      border-top: 1px solid $border
+      border-top: 1px solid $border;
     }
   }
 

+ 5 - 0
src/client/styles/scss/_me.scss

@@ -0,0 +1,5 @@
+.user-settings-page {
+  .grw-header.sticky-top {
+    height: unset;
+  }
+}

+ 3 - 3
src/client/styles/scss/_on-edit.scss

@@ -72,8 +72,8 @@ body.on-edit {
     }
   }
 
-  // show compact subnav
-  .grw-compact-subnav {
+  // show revision path
+  .grw-revision-path-for-edit {
     display: block !important;
   }
 
@@ -97,7 +97,7 @@ body.on-edit {
 
     background: none;
 
-    > .grw-title-bar {
+    > .grw-subnav-container {
       width: 100%; //   for crowi layout
       padding: 0; //    for crowi layout
       pointer-events: initial; // enable pointer-events

+ 0 - 41
src/client/styles/scss/_page_growi.scss

@@ -1,41 +0,0 @@
-.growi {
-  header {
-    // Adjust to be on top of the growi subnavigation
-    z-index: $zindex-sticky - 100;
-    ul.authors {
-      padding-left: 1.5em;
-      margin: 0;
-
-      li {
-        font-size: 12px;
-        list-style: none;
-      }
-
-      .picture {
-        width: 22px;
-        height: 22px;
-        border: 1px solid #ccc;
-
-        &.picture-xs {
-          width: 14px;
-          height: 14px;
-        }
-      }
-    }
-    .grw-compact-subnavbar {
-      h2 {
-        font-size: 20px;
-        line-height: 1.1em;
-        @include media-breakpoint-down(md) {
-          font-size: 18px;
-        }
-        @include media-breakpoint-down(sm) {
-          font-size: 14px;
-        }
-        @include media-breakpoint-down(xs) {
-          font-size: 12px;
-        }
-      }
-    }
-  }
-}

+ 0 - 10
src/client/styles/scss/_page_header.scss

@@ -1,10 +0,0 @@
-#page-header {
-  &:hover {
-    .btn-copy,
-    .btn-edit,
-    .btn-edit-tags {
-      // change button opacity
-      opacity: unset;
-    }
-  }
-}

+ 97 - 0
src/client/styles/scss/_subnav.scss

@@ -0,0 +1,97 @@
+$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
+
+@mixin setTransitionForCompactMode() {
+  // set transition-duration (normal -> compact)
+  transition: all 300ms $easeInOutCubic;
+}
+
+/*
+ * layout for sticky
+ */
+.grw-header.sticky-top {
+  // Adjust to be on top of the growi subnavigation
+  z-index: $zindex-sticky - 100;
+  height: 80px;
+  pointer-events: none; // disable pointer events for sticky
+
+  .grw-subnav {
+    overflow: unset;
+    pointer-events: all; // enable pointer events
+  }
+}
+
+/*
+ * Compact Mode Switching
+ */
+.grw-subnavbar {
+  &.grw-subnavbar-compact {
+    @include setTransitionForCompactMode();
+
+    h1 {
+      @include variable-font-size(18px);
+      @include setTransitionForCompactMode();
+    }
+  }
+}
+
+/*
+ * Sticky Mode Switching
+ */
+.grw-subnavbar {
+  &.grw-subnavbar-sticky {
+    // set transition-duration (init -> sticky)
+    transition: all 400ms linear !important;
+  }
+}
+
+/*
+ * Styles
+ */
+
+.grw-header {
+  .title {
+    padding: 0.5rem 15px;
+
+    line-height: 1em;
+
+    @include variable-font-size(28px);
+    line-height: 1.1em;
+  }
+}
+
+.grw-subnavbar {
+  &:hover {
+    .btn-copy,
+    .btn-edit,
+    .btn-edit-tags {
+      // change button opacity
+      opacity: unset;
+    }
+  }
+
+  h1 {
+    @include variable-font-size(28px);
+    line-height: 1.1em;
+  }
+
+  ul.authors {
+    padding-left: 1.5em;
+    margin: 0;
+
+    li {
+      font-size: 12px;
+      list-style: none;
+    }
+
+    .picture {
+      width: 22px;
+      height: 22px;
+      border: 1px solid #ccc;
+
+      &.picture-xs {
+        width: 14px;
+        height: 14px;
+      }
+    }
+  }
+}

+ 21 - 17
src/client/styles/scss/_tag.scss

@@ -1,31 +1,35 @@
-.tag-viewer {
-  .manage-tags {
-    font-size: 10px;
-    cursor: pointer;
+.tags-page {
+  .grw-header.sticky-top {
+    height: unset;
   }
 
-  .tag-icon:not(:first-child) {
-    margin-left: 5px;
-  }
+  .tag-viewer {
+    .manage-tags {
+      font-size: 10px;
+      cursor: pointer;
+    }
 
-  .btn.btn-edit-tags,
-  .tag-icon {
-    font-size: 10px;
-  }
+    .tag-icon:not(:first-child) {
+      margin-left: 5px;
+    }
 
-  .tag-name {
-    margin-left: 1px;
-    font-size: 10px;
+    .btn.btn-edit-tags,
+    .tag-icon {
+      font-size: 10px;
+    }
+
+    .tag-name {
+      margin-left: 1px;
+      font-size: 10px;
+    }
   }
-}
 
-#tags-page {
   .list-tag-count {
     background: rgba(0, 0, 0, 0.08);
   }
 }
 
-#editTagModal {
+#edit-tag-modal {
   .form-control {
     height: auto;
   }

+ 52 - 27
src/client/styles/scss/_user.scss

@@ -1,4 +1,49 @@
-.user-page-header {
+$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
+
+@mixin setTransitionForCompactMode() {
+  // set transition-duration (normal -> compact)
+  transition: all 300ms $easeInOutCubic;
+}
+
+.grw-header.grw-header-user-page {
+  height: 150px;
+}
+
+/*
+ * Compact Mode Switching
+ */
+.grw-subnavbar.grw-subnavbar-user-page {
+  &.grw-subnavbar-compact {
+    .grw-user-page-path {
+      margin-bottom: 0;
+      font-size: 14px;
+
+      @include setTransitionForCompactMode();
+    }
+    .picture {
+      width: 62px;
+      height: 62px;
+
+      @include setTransitionForCompactMode();
+    }
+    h1 {
+      font-size: 1.5em;
+      line-height: 30px;
+
+      @include setTransitionForCompactMode();
+    }
+    .users-meta {
+      margin-left: 15px;
+
+      @include setTransitionForCompactMode();
+    }
+  }
+}
+
+/*
+ * Styles
+ */
+.grw-subnavbar-user-page {
   #revision-path {
     margin-bottom: 0;
   }
@@ -14,19 +59,16 @@
   }
 
   .picture {
-    width: 64px;
-    height: 64px;
+    width: 72px;
+    height: 72px;
   }
 
-  .user-page-meta {
+  ul.user-page-meta {
+    padding-left: 0;
     color: #999;
 
-    ul {
-      padding-left: 0;
-
-      li {
-        list-style: none;
-      }
+    li {
+      list-style: none;
     }
 
     .user-page-username {
@@ -55,23 +97,6 @@
   }
 }
 
-// affix
-.user-page-header.affix {
-  .users-meta {
-    margin-left: 15px;
-  }
-
-  h1 {
-    font-size: 1.5em;
-    line-height: 30px;
-  }
-
-  .picture {
-    width: 48px;
-    height: 48px;
-  }
-}
-
 .draft-list-item {
   .icon-container {
     .icon-copy,

+ 0 - 7
src/client/styles/scss/_user_growi.scss

@@ -1,11 +1,4 @@
 .growi .user-page {
-  // affix
-  .user-page-header.affix {
-    #revision-path {
-      display: none;
-    }
-  }
-
   .revision-toc {
     position: sticky;
     top: 105px;

+ 2 - 2
src/client/styles/scss/style-app.scss

@@ -41,16 +41,16 @@
 @import 'layout_kibela';
 @import 'layout_variable';
 @import 'login';
+@import 'me';
 @import 'navbar';
 @import 'navbar_kibela';
 @import 'notification';
 @import 'on-edit';
 @import 'page_list';
 @import 'page';
-@import 'page_header';
-@import 'page_growi';
 @import 'search';
 @import 'shortcuts';
+@import 'subnav';
 @import 'tag';
 @import 'user';
 @import 'user_growi';

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

@@ -1,10 +1,3 @@
-.logo {
-  .logo-mark,
-  .logo-text {
-    fill: white;
-  }
-}
-
 /*
  * Button
  */
@@ -111,15 +104,6 @@ textarea.form-control {
   // border: 1px solid $border;
 }
 
-/*
- * GROWI header
- */
-header.affix {
-  .logo-mark {
-    fill: white;
-  }
-}
-
 /*
  * GROWI page list
  */
@@ -139,9 +123,24 @@ header.affix {
 /*
  * GROWI subnavigation
  */
-.grw-compact-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 90%), 0.9);
-  box-shadow: 0 0 2px darken($bgcolor-global, 5%);
+.admin-page,
+.user-settings-page,
+.tags-page {
+  .grw-header {
+    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
+  }
+}
+
+.grw-subnavbar {
+  background-color: rgba(darken($bgcolor-global, 90%), 1);
+
+  &.grw-subnavbar-sticky {
+    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
+    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 5%);
+  }
+}
+
+.grw-subnavbar-sticky {
 }
 
 /*

+ 15 - 12
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -11,15 +11,6 @@
   background-color: darken($bgcolor-global, 5%);
 }
 
-/*
- * GROWI header
- */
-header.affix {
-  .logo-mark {
-    fill: theme-color('primary');
-  }
-}
-
 /*
  * GROWI search-top
  */
@@ -35,9 +26,21 @@ header.affix {
 /*
  * GROWI subnavigation
  */
-.grw-compact-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 6%), 0.9);
-  box-shadow: 0 0 2px darken($bgcolor-global, 40%);
+.admin-page,
+.user-settings-page,
+.tags-page {
+  .grw-header {
+    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
+  }
+}
+
+.grw-subnavbar {
+  background-color: rgba(darken($bgcolor-global, 5%), 1);
+
+  &.grw-subnavbar-sticky {
+    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
+    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 40%);
+  }
 }
 
 /*

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

@@ -85,10 +85,6 @@ $link-hover-color: $color-link-hover;
   }
 }
 
-.grw-title-bar {
-  background: darken($bgcolor-global, 2%);
-}
-
 .grw-sidebar {
   .grw-logo {
     background-color: darken($bgcolor-navbar, 10%);
@@ -210,13 +206,6 @@ $link-hover-color: $color-link-hover;
   }
 }
 
-/*
- * GROWI header
- */
-header.affix {
-  background: rgba(darken($bgcolor-global, 2%), 0.9);
-}
-
 /*
  * GROWI on-edit
  */

+ 1 - 3
src/server/views/admin/Users_reserve.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
-</header>
+<h1 class="title">{{ t('User_Management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 3
src/server/views/admin/app.html

@@ -6,11 +6,10 @@
 {% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('App settings') }}</h1>
-</header>
+<h1 class="title">{{ t('App settings') }}</h1>
 {% endblock %}
 
+
 {% block content_main %}
 <div class="content-main admin-app row">
   {% parent %}

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

@@ -13,9 +13,7 @@
 {% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Customize') }}</h1>
-</header>
+<h1 class="title">{{ t('Customize') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/export.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Export Archive Data')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Export Archive Data') }}</h1>
-</header>
+<h1 class="title">{{ t('Export Archive Data') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/external-accounts.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('external_account_management')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
-</header>
+<h1 class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/global-notification-detail.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
-</header>
+<h1 class="title">{{ t('Notification settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/importer.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Import Data')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Import Data') }}</h1>
-</header>
+<h1 class="title">{{ t('Import Data') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/index.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Management Wiki Home')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title"> {{ t('Management Wiki Home') }}</h1>
-</header>
+<h1 class="title"> {{ t('Management Wiki Home') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/markdown.html

@@ -4,9 +4,7 @@
  · {{ path }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Markdown Settings') }}</h1>
-</header>
+<h1 class="title">{{ t('Markdown Settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/notification.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
-</header>
+<h1 class="title">{{ t('Notification settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/search.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Full Text Search management')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('Full Text Search management') }}</h1>
-</header>
+<h1 class="title">{{ t('Full Text Search management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/security.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('security_settings')) }} · {% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('security_settings') }}</h1>
-</header>
+<h1 class="title">{{ t('security_settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/user-group-detail.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('UserGroup Management') + '/' + userGroup.name | preventXss }}</h1>
-</header>
+<h1 class="title">{{ t('UserGroup Management') + '/' + userGroup.name | preventXss }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/user-groups.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('UserGroup Management') }}</h1>
-</header>
+<h1 class="title">{{ t('UserGroup Management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 3
src/server/views/admin/users.html

@@ -3,9 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
 
 {% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
-</header>
+<h1 class="title">{{ t('User_Management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 1
src/server/views/layout-crowi/base/layout.html

@@ -31,7 +31,7 @@
   </aside>
 
   <div class="row grw-subnav">
-    <div class="col-md-9 grw-title-bar">
+    <div class="col-md-9">
       {% block content_header %}
       {% endblock %}
     </div>

+ 6 - 7
src/server/views/layout-growi/base/layout.html

@@ -7,14 +7,13 @@
 {% endblock %}
 
 {% block layout_main %}
-<header class="sticky-top py-0" id="page-header">
-  <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 '/' === path.slice(-1) %}
-      {% include '../../widget/create_portal.html' %}
-    {% endif %}
-  {% endif %}
+
+{% block content_header_wrapper %}
+<header class="sticky-top py-0 grw-header">
+  {% block content_header %}
+  {% endblock %}
 </header>
+{% endblock %}
 
 <div class="container-fluid">
   <div class="row">

+ 8 - 6
src/server/views/layout-growi/user_page.html

@@ -5,12 +5,14 @@
   user-page
 {% endblock %}
 
-{% block content_header %}
-  {% if pageUser %}
-    <header id="grw-subnav-for-user-page" class="user-page-header" data-page-user="{{ pageUser|json }}"></header>
-  {% else %}
-    {% parent %}
-  {% endif %}
+{% block content_header_wrapper %}
+  <header class="sticky-top py-0 grw-header grw-header-user-page">
+    {% if pageUser %}
+      <div id="grw-subnav-for-user-page" class="grw-subnav" data-page-user="{{ pageUser|json }}"></div>
+    {% else %}
+      {% parent %}
+    {% endif %}
+  </header>
 {% endblock %}
 
 

+ 6 - 10
src/server/views/layout-growi/widget/header.html

@@ -1,11 +1,7 @@
-<header id="page-header">
+<div id="grw-subnav" class="grw-subnav" data-is-forbidden-page="{{ forbidden }}"></div>
 
-  <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 '/' === path.slice(-1) %}
-        {% include '../../widget/create_portal.html' %}
-      {% endif %}
-    {% endif %}
-
-</header>
+{% if not page and not forbidden and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
+  {% if '/' === path.slice(-1) %}
+    {% include '../../widget/create_portal.html' %}
+  {% endif %}
+{% endif %}

+ 1 - 1
src/server/views/layout-kibela/base/layout.html

@@ -13,7 +13,7 @@
 
     <div id="main" class="main col-12 kibela-block bg-white round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
       <div class="row grw-subnav">
-        <div class="col-12 grw-title-bar">
+        <div class="col-12">
           {% block content_header %} {% endblock %}
         </div>
       </div>

+ 1 - 1
src/server/views/layout-kibela/user_page.html

@@ -7,7 +7,7 @@
 
 {% block content_header %}
   {% if pageUser %}
-    <header id="grw-subnav-for-user-page" class="user-page-header" data-page-user="{{ pageUser|json }}"></header>
+    <header id="grw-subnav-for-user-page" class="grw- subnav grw-subnav-user-page" data-page-user="{{ pageUser|json }}"></header>
   {% else %}
     {% parent %}
   {% endif %}

+ 0 - 85
src/server/views/me/api_token.html

@@ -1,85 +0,0 @@
-{% extends '../layout-growi/base/layout.html' %}
-
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('API Settings')) }}{% endblock %}
-
-
-{% block content_header %}
-<header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('API Settings') }}</h1>
-</header>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-
-  <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="icon-lock"></i> {{ t('Password Settings') }}</a></li>
-    <li class="active"><a href="/me/apiToken"><i class="icon-paper-plane"></i> {{ t('API Settings') }}</a></li>
-  </ul>
-
-  <div class="tab-content">
-
-  {% set message = req.flash('successMessage') %}
-  {% if message.length %}
-  <div class="alert alert-success grw-mt-10px">
-    {{ message }}
-  </div>
-  {% endif %}
-
-  {% if req.form.errors.length > 0 %}
-  <div class="alert alert-danger grw-mt-10px">
-    <ul>
-    {% for error in req.form.errors %}
-      <li>{{ error }}</li>
-    {% endfor %}
-    </ul>
-  </div>
-  {% endif %}
-
-  <div class="form-box m-t-20">
-
-    <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
-    <fieldset>
-      <legend>{{ t('API Token Settings') }}</legend>
-      <div class="form-group {% if not user.password %}has-error{% endif %}">
-        <label for="" class="col-xs-3 control-label">{{ t('Current API Token') }}</label>
-        <div class="col-xs-6">
-          {% if user.apiToken %}
-            <input class="form-control" type="text" value="{{ user.apiToken }}">
-          {% else %}
-          <p class="form-control-static">
-            {{ t('page_me_apitoken.notice.apitoken_issued') }}
-          </p>
-          {% endif %}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <div class="col-xs-offset-3 col-xs-9">
-
-          <p class="alert alert-warning">
-            {{ t('page_me_apitoken.notice.update_token1') }}<br>
-            {{ t('page_me_apitoken.notice.update_token2') }}
-          </p>
-
-          <button type="submit" value="1" name="apiTokenForm[confirm]" class="btn btn-primary">{{ t('Update API Token') }}</button>
-        </div>
-      </div>
-
-    </fieldset>
-    </form>
-  </div>
-
-
-  </div>
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock %}
-
-{% block layout_footer %}
-{% endblock %}

+ 0 - 249
src/server/views/me/external-accounts.html

@@ -1,249 +0,0 @@
-{% extends '../layout-growi/base/layout.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('user_management.external_account')) }}{% endblock %}
-
-{% block content_header %}
-<header id="page-header">
-  <h1 id="mypage-title" class="title">{{ t('user_management.external_account') }}</h1>
-</header>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-
-  <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
-    <li class="active"><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="icon-lock"></i> {{ t('Password Settings') }}</a></li>
-    <li><a href="/me/apiToken"><i class="icon-paper-plane"></i> {{ t('API Settings') }}</a></li>
-  </ul>
-
-  <div class="tab-content">
-
-  {% set couldntDisassociateError = req.flash('couldntDisassociateError') %}
-  {% if couldntDisassociateError != null %}
-  <div class="alert alert-danger grw-mt-10px">
-    <b>Couldn't disassociate External Account</b><br>
-    You have not set a password and have only one External Account.
-  </div>
-  {% endif %}
-
-  {% set error = req.flash('errorMessage') %}
-  {% if error.length %}
-  {% for e in error %}
-  <div class="alert alert-danger grw-mt-10px">
-    <b>Server Error occured:</b><br>
-    {{ e }}
-  </div>
-  {% endfor %}
-  {% endif %}
-
-  {% set warn = req.flash('warningMessage') %}
-  {% if warn.length %}
-  {% for w in warn %}
-  <div class="alert alert-warning grw-mt-10px">
-    {{ w }}
-  </div>
-  {% endfor %}
-  {% endif %}
-
-  {% set message = req.flash('successMessage') %}
-  {% if message.length %}
-  <div class="alert alert-success grw-mt-10px">
-    <b>{{ message }}</b>
-  </div>
-  {% endif %}
-
-
-
-  <legend class="m-t-20" style="line-height: 1.7em;">
-    <button class="btn btn-default btn-sm float-right" data-target="#create-external-account" data-toggle="modal">
-      <i class="icon-plus" aria-hidden="true"></i>
-      Add
-    </button>
-    {{ t('External Accounts') }}
-  </legend>
-
-  <div class="row">
-    <div class="col-md-12">
-      <table class="table table-bordered table-user-list">
-        <thead>
-          <tr>
-            <th width="120px">Authentication Provider</th>
-            <th>
-              <code>accountId</code>
-            </th>
-            <th width="200px">{{ t('Created') }}</th>
-            <th width="150px">{{ t('Admin') }}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for account in externalAccounts %}
-          <tr>
-            <td>{{ account.providerType }}</td>
-            <td>
-              <strong>{{ account.accountId }}</strong>
-            </td>
-            <td>{{ account.createdAt|datetz('Y-m-d') }}</td>
-            <td class="text-center">
-              <button class="btn btn-default btn-sm btn-danger"
-                  data-toggle="modal" data-target="#diassociate-external-account" data-provider-type="{{ account.providerType }}" data-account-id="{{ account.accountId }}">
-                <i class="ti-unlink"></i>
-                {{ t('Diassociate') }}
-              </button>
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-    </div>
-  </div>
-
-  {# modal #}
-  <style>
-    .modal.create-external-account .modal-dialog {
-      width: 750px;
-    }
-  </style>
-  <div class="modal create-external-account" id="create-external-account">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-        <div class="modal-header bg-info">
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">{{ t('Create External Account') }}</div>
-        </div>
-
-        <div class="modal-body">
-
-          <ul class="nav nav-tabs passport-settings" role="tablist">
-            <li class="active">
-              <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="fa fa-sitemap"></i> LDAP</a>
-            </li>
-            <li class="tbd">
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> (TBD) GitHub</a>
-            </li>
-            <li class="tbd">
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="fa fa-google"></i> (TBD) Google OAuth</a>
-            </li>
-            <li class="tbd">
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
-            </li>
-            <li class="tbd">
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> (TBD) Twitter</a>
-            </li>
-          </ul>
-
-          <div class="tab-content passport-settings mt-4">
-            <div id="passport-ldap" class="tab-pane active" role="tabpanel" >
-              <div id="formLdapAssociationContainer">
-                {% include '../widget/passport/ldap-association-tester.html' %}
-                <div class="clearfix">
-                  <button type="button" class="btn btn-info float-right" onclick="associateLdap()">
-                    <i class="fa fa-plus-circle" aria-hidden="true"></i>
-                    {{ t('add') }}
-                  </button>
-                </div>
-              </div>
-            </div>
-
-            <div id="passport-google-oauth" class="tab-pane" role="tabpanel">
-              (TBD)
-            </div>
-
-            <div id="passport-github" class="tab-pane" role="tabpanel">
-              (TBD)
-            </div>
-
-            <div id="passport-facebook" class="tab-pane" role="tabpanel">
-              (TBD)
-            </div>
-
-            <div id="passport-twitter" class="tab-pane" role="tabpanel">
-              (TBD)
-            </div>
-
-          </div><!-- /.tab-content -->
-
-        </div><!-- /.modal-body -->
-
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-
-    <script>
-      /**
-       * associate (submit the form)
-       */
-      function associateLdap() {
-        var $form = $('#formLdapAssociationContainer > form');
-        var $action = '/me/external-accounts/associateLdap';
-        $form.attr('action', $action);
-        $form.submit();
-      }
-    </script>
-
-  </div><!-- /.modal -->
-
-  <div class="modal diassociate-external-account" id="diassociate-external-account">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-        <div class="modal-header">
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">{{ t('Diassociate External Account') }}</div>
-        </div>
-
-        <div class="modal-body">
-          <div class="row">
-            <div class="col-md-12">
-              <p><b>
-                Are you sure to diassociate the
-                <span class="diassociate-provider-type"></span> account
-                <code class="diassociate-account-id"></code>?
-              </b></p>
-            </div>
-          </div>
-        </div>
-
-        <div class="modal-footer">
-          <form action="/me/external-accounts/disassociate" method="post">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <input type="hidden" name="providerType">
-            <input type="hidden" name="accountId">
-            <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">
-              {{ t('Cancel') }}
-            </button>
-            <button type="submit" class="btn btn-sm btn-danger">
-              <i class="ti-unlink"></i>
-              {{ t('Diassociate') }}
-            </button>
-          </form>
-        </div>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-
-    <script>
-      $('#diassociate-external-account').on('show.bs.modal', function (event) {
-        var modal = $(this);
-        var button = $(event.relatedTarget); // Button that triggered the modal
-        // get data-*
-        var providerType = button.data('provider-type');
-        var accountId = button.data('account-id');
-        // set labels
-        modal.find('.diassociate-provider-type').text(providerType);
-        modal.find('.diassociate-account-id').text(accountId);
-        // set hidden inputs
-        modal.find('input:hidden[name="providerType"]').val(providerType);
-        modal.find('input:hidden[name="accountId"]').val(accountId);
-      })
-    </script>
-  </div><!-- /.modal -->
-
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock %}
-
-{% block layout_footer %}
-{% endblock %}

+ 4 - 354
src/server/views/me/index.html

@@ -2,364 +2,14 @@
 
 {% block html_title %}{{ customizeService.generateCustomTitle(t('User Settings')) }}{% endblock %}
 
+{% block html_base_css %}user-settings-page{% endblock %}
+
 {% block content_header %}
-<header id="page-header">
-  <h1 id="mypage-title" class="title">{{ t('User Settings') }}</h1>
-</header>
+<h1 class="title">{{ t('User Settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main" id="personal-setting">
-
-  <ul class="nav nav-tabs mb-4" role="tablist">
-    <li class="nav-item">
-      <a class="nav-link active" href="/me" role="tab" data-toggle="tab"><i class="icon-user"></i> {{ t('User Information') }}</a>
-    </li>
-    <li class="nav-item">
-      <a class="nav-link" href="/me/external-accounts" role="tab" data-toggle="tab"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a>
-    </li>
-    <li class="nav-item">
-      <a class="nav-link" href="/me/password" role="tab" data-toggle="tab"><i class="icon-lock"></i> {{ t('Password Settings') }}</a>
-    </li>
-    <li class="nav-item">
-      <a class="nav-link" href="/me/apiToken" role="tab" data-toggle="tab"><i class="icon-paper-plane"></i> {{ t('API Settings') }}</a>
-    </li>
-  </ul>
-
-  <div class="tab-content">
-
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success grw-mt-10px">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set wmessage = req.flash('warningMessage') %}
-  {% if wmessage.length %}
-  <div class="alert alert-danger grw-mt-10px">
-    {{ wmessage }}
-  </div>
-  {% endif %}
-
-  {% if req.form.errors.length > 0 %}
-  <div class="alert alert-danger grw-mt-10px">
-    <ul>
-    {% for error in req.form.errors %}
-      <li>{{ error }}</li>
-    {% endfor %}
-    </ul>
-  </div>
-  {% endif %}
-
-
-  <div class="form-box mt-3">
-    <form action="/me" method="post" role="form">
-      <fieldset>
-        <legend class="border-bottom mb-4">{{ t('Basic Info') }}</legend>
-        <div class="form-group row">
-          <label for="userForm[name]" class="col-sm-2 col-form-label">Name</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" name="userForm[name]" value="{{ user.name }}" required>
-          </div>
-        </div>
-        <div class="form-group">
-          <div class="row">
-            <label for="userForm[email]" class="col-sm-2 col-form-label">Email</label>
-            <div class="col-sm-4">
-              <input type="email" class="form-control" name="userForm[email]" value="{{ user.email }}" aria-describedby="userForm[email]" required>
-            </div>
-          </div>
-          <div class="offset-sm-2 col-sm-10">
-            {% if getConfig('crowi', 'security:registrationWhiteList') && getConfig('crowi', 'security:registrationWhiteList').length %}
-            <p id="userForm[email]" class="form-text text-muted">
-              {{ t('page_register.form_help.email') }}
-            <ul>
-              {% for em in getConfig('crowi', 'security:registrationWhiteList') %}
-              <li><code>{{ em }}</code></li>
-              {% endfor %}
-            </ul>
-            </p>
-            {% endif %}
-          </div>
-        </div>
-        <div class="form-group row">
-          <label for="userForm[isEmailPublished]" class="col-sm-2 col-form-label">{{ t('Disclose E-mail') }}</label>
-          <div class="col-sm-4">
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioEmailShow"
-                name="userForm[isEmailPublished]"
-                value="{{ true }}"
-                {% if user.isEmailPublished == true %}checked="checked"{% endif %}
-                class="custom-control-input"
-              > 
-              <label class="custom-control-label" for="radioEmailShow">{{ t('Show') }}</label>
-            </div>
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioEmailHide"
-                name="userForm[isEmailPublished]"
-                value="{{ false }}"
-                {% if user.isEmailPublished == false %}checked="checked"{% endif %}
-                class="custom-control-input"
-              >
-              <label class="custom-control-label" for="radioEmailHide">{{ t('Hide') }}</label>
-            </div>
-          </div>
-        </div>
-        <div class="form-group row {% if not user.lang %}has-error{% endif %}">
-          <label for="userForm[lang]" class="col-sm-2 col-form-label">Language</label>
-          <div class="col-sm-4">
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioLangEn"
-                name="userForm[lang]"
-                value="{{ consts.language.LANG_EN_US }}"
-                class="custom-control-input"
-                {% if user.lang == consts.language.LANG_EN_US %}checked="checked"{% endif %}
-              >
-              <label class="custom-control-label" for="radioLangEn">{{ t('English') }}</label>
-            </div>
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioLangJa"
-                name="userForm[lang]"
-                value="{{ consts.language.LANG_JA }}"
-                class="custom-control-input"
-                {% if user.lang == consts.language.LANG_JA %}checked="checked"{% endif %}
-              >
-              <label class="custom-control-label" for="radioLangJa">{{ t('Japanese') }}</label>
-            </div>
-          </div>
-        </div>
-
-        <div class="form-group row">
-          <div class="offset-sm-2 col-sm-10">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-          </div>
-        </div>
-      </fieldset>
-    </form>
-  </div>
-
-  <div class="form-box mt-3">
-
-    <!-- separeted form tag -->
-    <form action="/me/imagetype" id="formImageType" method="post" class="form" role="form"></form>
-
-    <fieldset>
-      <legend class="border-bottom mb-5">{{ t('Set Profile Image') }}</legend>
-      <div class="form-group row">
-        <div class="col-sm-4 offset-sm-1">
-          <h4>
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioGravatar"
-                form="formImageType"
-                name="imagetypeForm[isGravatarEnabled]"
-                value="true"
-                {% if user.isGravatarEnabled %}checked="checked"{% endif %}
-                class="custom-control-input"
-              >
-              <label class="custom-control-label custom-control-inline" for="radioGravatar">
-                <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" />
-                <span class="pl-1">Gravatar</span>
-              </label>
-              <a href="https://gravatar.com/">
-                <small>
-                  <i class="icon-arrow-right-circle" aria-hidden="true"></i>
-                </small>
-              </a>
-            </div>
-          </h4>
-  
-          <img src="{{ user|gravatar }}" width="64">
-        </div>
-        <div class="col-sm-7">
-          <h4>
-            <div class="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radioUploadPicture"
-                form="formImageType"
-                name="imagetypeForm[isGravatarEnabled]"
-                value="false"
-                {% if !user.isGravatarEnabled  %}checked="checked"{% endif %}
-                class="custom-control-input"
-              >
-              <label for="radioUploadPicture" class="custom-control-label">{{ t('Upload Image') }}</label>
-            </div>
-          </h4>
-          <div class="form-group">
-            <div id="pictureUploadFormMessage" class=""></div>
-            <div class="row">
-              <label for="" class="col-sm-4">{{ t('Current Image') }}</label>
-              <div class="col-sm-8">
-                <p><img src="{{ user|uploadedpicture }}" class="picture picture-lg rounded-circle" id="settingUserPicture"></p>
-                <form
-                  id="remove-attachment"
-                  action="/_api/attachments.removeProfileImage"
-                  method="post"
-                  class="form-horizontal"
-                  style="{% if not user.imageAttachment %}display: none{% endif %}"
-                >
-                  <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  <button type="submit" class="btn btn-danger">{{ t('Delete Image') }}</button>
-                </form>
-              </div>
-            </div>
-          </div>
-        </h4>
-
-        <img src="{{ user|gravatar }}" width="64">
-      </div><!-- /.col-sm* -->
-
-      <div class="form-group col-md-4 col-sm-7">
-        <h4>
-          <div class="radio radio-primary">
-            <input type="radio" id="radioUploadPicture" form="formImageType" name="imagetypeForm[isGravatarEnabled]" value="false" {% if !user.isGravatarEnabled  %}checked="checked"{% endif %}>
-            <label for="radioUploadPicture">
-              {{ t('Upload Image') }}
-            </label>
-          </div>
-        </h4>
-        <div class="form-group">
-          <div id="pictureUploadFormMessage"></div>
-          <label for="" class="col-sm-4 control-label">
-            {{ t('Current Image') }}
-          </label>
-          <div class="col-sm-8">
-            <p>
-            <img src="{{ user|uploadedpicture }}" class="picture picture-lg img-circle" id="settingUserPicture"><br>
-            </p>
-            <p>
-            <form id="remove-attachment" action="/_api/attachments.removeProfileImage" method="post" class="form-horizontal"
-                style="{% if not user.imageAttachment %}display: none{% endif %}">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <button type="submit" class="btn btn-danger">{{ t('Delete Image') }}</button>
-            </form>
-            </p>
-          </div>
-        </div><!-- /.form-group -->
-
-        <div class="form-group">
-          <div id="profile-image-uploader"></div>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <div class="offset-sm-4 col-sm-6">
-          <button type="submit" form="formImageType" class="btn btn-primary">{{ t('Update') }}</button>
-        </div>
-      </div>
-    </fieldset>
-  </div><!-- /.form-box -->
-
-  <script>
-    $("#pictureUploadForm input[name=profileImage]").on('change', function(){
-      if ($(this).val() == '') {
-        return false;
-      }
-
-      var $form = $('#pictureUploadForm');
-      var formData = new FormData();
-      formData.append('file', this.files[0]);
-      formData.append('_csrf', document.getElementsByName("_csrf")[0].value);
-
-      $('#pictureUploadFormProgress').html('<div class="speeding-wheel-sm mr-2"></div> アップロード中...');
-      $.ajax($form.attr("action"), {
-        type: 'post',
-        processData: false,
-        contentType: false,
-        data: formData
-      })
-      .then(function(data) {
-        if (data.ok) {
-          var attachment = data.attachment;
-          $('#settingUserPicture').attr('src', attachment.filePathProxied + '?time=' + (new Date()));
-          $('form#remove-attachment').show();
-          $('form#remove-attachment input[name=attachment_id]').val(attachment.id);
-          $('#pictureUploadFormMessage')
-            .addClass('alert alert-success')
-            .html('変更しました');
-        }
-        else {
-          throw new Error('statis is invalid');
-        }
-      })
-      .catch(function(err) {
-        $('#pictureUploadFormMessage')
-          .addClass('alert alert-danger')
-          .html('変更中にエラーが発生しました。');
-      })
-      // finally
-      .then(function() {
-        $('#pictureUploadFormProgress').html('');
-      });
-      return false;
-    });
-
-    $('form#remove-attachment').on('submit', function(event) {
-      // process with jQuery
-      event.preventDefault();
-
-      $.post($(this).attr('action'), $(this).serializeArray())
-      .then(function(data) {
-        if (data.ok) {
-          $('#settingUserPicture').attr('src', '/images/icons/user.svg');
-          $('form#remove-attachment').hide();
-        }
-        else {
-          throw new Error('statis is invalid');
-        }
-      })
-      .catch(function(err) {
-        $('#pictureUploadFormMessage')
-          .addClass('alert alert-danger')
-          .html('変更中にエラーが発生しました。');
-      })
-    });
-  </script>
-
-  </div> {# end of .tab-contents #}
-
-  {#
-  <div class="form-box">
-    <form action="/me/username" method="post" class="form-horizontal" role="form">
-      <fieldset>
-        <legend>ユーザーID (ユーザー名) の変更</legend>
-      <div class="form-group">
-        <label for="userNameForm[username]" class="col-sm-2 control-label">ユーザーID</label>
-        <div class="col-sm-4">
-          <input class="form-control" type="text" name="userNameForm[username]" value="{{ user.username }}" required>
-          <p class="help-block">すべてのマイページの</p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <div class="col-sm-offset-2 col-sm-10">
-          <p class="alert alert-warning">
-          ユーザーIDを変更すると、<code>/user/{{ user.username }}</code> 以下のページがすべて <code>/user/新しいユーザーID</code> の下に移動されます。<br>
-          また、これまでのページにリダイレクトは設定されず、この操作の取り消しもできません。<br>
-          実行には十分に注意をしてください。
-          </p>
-          <button type="submit" class="btn btn-warning">ユーザーIDの変更を実行する</button>
-        </div>
-      </div>
-    </fieldset>
-    </form>
-  </div>
-  #}
-
-  </div>
-</div>
+<div class="content-main" id="personal-setting"></div>
 {% endblock content_main %}
 
 {% block content_footer %}

+ 8 - 9
src/server/views/tags.html

@@ -1,16 +1,15 @@
 {% extends 'layout/layout.html' %}
 
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Tags')) }}{% endblock %}
+
+{% block html_base_css %}tags-page{% endblock %}
+
 {% block layout_main %}
+<header class="sticky-top py-0 grw-header">
+  <h1 class="title">{{ t('Tags') }}</h1>
+</header>
+
 <div class="container-fluid">
-  <div class="row grw-subnav">
-    <div class="col-xs-12 grw-title-bar">
-      {% block content_header %}
-      <header id="page-header">
-        <h1 id="admin-title" class="title">{{ t('Tags') }}</h1>
-      </header>
-      {% endblock %}
-    </div>
-  </div>
   <div class="row">
     <div id="main" class="main mt-3 col-md-12 tags-page">
       <div class="" id="tags-page"></div>

+ 27 - 25
src/server/views/widget/page_tabs.html

@@ -11,31 +11,33 @@
   </li>
 
   {% if !isTrashPage() %}
-  <li class="nav-item grw-main-nav-item-left grw-nav-item-edit">
-    <a
-      {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %}
-        class="edit-button edit-button-disabled"
-        data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-      {% endif %}
-    >
-      <i class="icon-note"></i> {{ t('Edit') }}
-    </a>
-  </li>
-  {% if isHackmdSetup() %}
-  <li class="nav-item grw-main-nav-item-left grw-nav-tab-hackmd">
-    <a
-      {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %}
-        class="edit-button edit-button-disabled"
-        data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-      {% endif %}
-    >
-      <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
-    </a>
-  </li>
-  {% endif %}
-    <div class="grw-compact-subnav d-none ml-2">
+    <li class="nav-item grw-main-nav-item-left grw-nav-item-edit">
+      <a
+        {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
+        {% if not user %}
+          class="edit-button edit-button-disabled"
+          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
+        {% endif %}
+      >
+        <i class="icon-note"></i> {{ t('Edit') }}
+      </a>
+    </li>
+
+    {% if isHackmdSetup() %}
+    <li class="nav-item grw-main-nav-item-left grw-nav-tab-hackmd">
+      <a
+        {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
+        {% if not user %}
+          class="edit-button edit-button-disabled"
+          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
+        {% endif %}
+      >
+        <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
+      </a>
+    </li>
+    {% endif %}
+
+    <div class="grw-revision-path-for-edit d-none ml-2">
       <h4 id="revision-path" class="mb-0"></h4>
       <div id="tag-label"></div>
     </div>